Architecture overview
Eclipse Atesor™ runs each port as a LangGraph state graph in src/graph.py. The graph has 13 nodes and 9 routing functions, and 5 nodes can call an LLM. Read this page to know which node runs when, and why a run finishes or escalates.
What is the state graph?
Section titled “What is the state graph?”LangGraph is a Python library that runs a graph of functions over one shared state object. Atesor builds a StateGraph(AgentState) in create_workflow(). Each node is a Python function that receives the AgentState, changes it, and returns it. A routing function reads the state after a node and returns the name of the next node.
The build, verify and fix steps form a separate compiled graph, the subgraph, from create_build_fix_subgraph(). The parent graph uses the compiled subgraph as one node, build_fix_subgraph.
main.py runs the graph with app.stream(initial_state, config={"recursion_limit": 120}). The real limit on a run is max_attempts (default 5, flag --max-attempts) and the cost limit. See Agent state for both checks.
How many nodes and routing functions are there?
Section titled “How many nodes and routing functions are there?”These counts come from create_workflow() and create_build_fix_subgraph() on main:
- The parent graph registers 10 nodes. One of them is the compiled subgraph.
- The subgraph registers 3 nodes.
- The total is 13 registered nodes, backed by 12 node functions and 1 compiled subgraph.
- There are 9 routing functions: 6 in the parent graph and 3 in the subgraph.
- The parent graph also has 4 fixed edges:
scout_nodetosupervisor_node,build_fix_subgraphtosupervisor_node,optimize_nodetoEND, andescalate_nodetoEND. - Two nodes run only with
--optimize:cached_recipe_nodeandoptimize_node.
| Node | Graph | Calls an LLM | What it does |
|---|---|---|---|
init_node |
parent | No | Clones or updates the repository and runs the scripted analysis. |
cached_recipe_node |
parent | No | With --optimize, turns a verified recipe cache entry into the build plan. |
analyst_node |
parent | Yes, planner role | Reads build files and docs, and writes a PackageAnalysis. |
heuristic_plan_node |
parent | No | Writes a default BuildPlan when it is safe. |
scout_node |
parent | Yes, scout role | Writes a custom BuildPlan from the evidence. |
supervisor_node |
parent | No | Logs a summary. Its routing function picks the next node. |
build_fix_subgraph |
parent | Through fix_node |
Runs the build, verify and fix loop. |
finish_node |
parent | Yes, summarizer role | Runs package tests, writes the recipe, and saves learning data. |
optimize_node |
parent | Yes, planner and builder roles | Runs performance mode after a finished port. |
escalate_node |
parent | No | Writes the escalation report. |
build_node (builder_node) |
subgraph | No | Runs the plan commands and the recovery heuristics. |
verify_node |
subgraph | No | Reads the ELF header of every build output. |
fix_node (fixer_node) |
subgraph | Yes, fixer role | Proposes one fix strategy and applies it. |
finish_node can make two LLM calls: one for the recipe and one for the artifact curator. Both use the summarizer role. optimize_node calls a model only when the workload of performance mode comes from the optimizer agent. cached_recipe_node runs under the planner role label, but it makes no LLM call. The model for each role is on the LLM layer page.
What does the flow look like?
Section titled “What does the flow look like?”The picture shows the parent graph, the subgraph, and the steps before and after the graph. The text diagrams after it give each routing condition.
The parent graph:
atesor-ai --repo <url> | |-- recipe cache hit, without --optimize and without --force | (main.py, before the graph runs) | write <repo>_recipe.md, exit 0 (verified) or 3 (not verified) | |-- --optimize without --force: main.py puts a verified | cache entry into state.cached_recipe vinit_node ............................... no LLM | route_init_to_next |-- FAILED or ESCALATED ---------------------------> escalate_node --> END |-- cached_recipe is set --> cached_recipe_node ... no LLM | | route_cached_recipe_to_next | |-- build_plan is set --> supervisor_node | |-- no build_plan ------> analyst_node vanalyst_node ............................ LLM (planner role) | route_analyst_to_next |-- no package_analysis ---------------------------> escalate_node --> END vheuristic_plan_node ..................... no LLM | route_heuristic_plan_to_next |-- no build_plan --> scout_node ...... LLM (scout role) | | v vsupervisor_node ......................... no LLM <------------------+ | route_supervisor_to_next | |-- SUCCESS -----------------------------> finish_node | |-- ESCALATED, or an escalation check ---> escalate_node --> END | |-- no package_analysis -----------------> analyst_node | |-- FAILED and a new plan is needed -----> scout_node | |-- FAILED, or plan PENDING / BUILDING --> build_fix_subgraph ----+ |-- all other states --------------------> scout_node
finish_node ............................. LLM (summarizer role) | route_finish_to_next |-- optimize_requested (--optimize) -----> optimize_node --> END |-- all other states --------------------> END
optimize_node ........................... LLM (planner and builder roles), only for a workload from the agentThe subgraph:
build_fix_subgraph (entry point: build_node)
build_node ........ no LLM | route_build_result |-- SUCCESS ------------------------------> verify_node |-- ESCALATED, or no attempts remain -----> __end__ |-- all other states ---------------------> fix_node
verify_node ....... no LLM | route_verify_result |-- SUCCESS or ESCALATED -----------------> __end__ |-- no attempts remain -------------------> __end__ |-- a fix exists and the error category | is not LINKING, COMPILATION or | ARCHITECTURE -------------------------> __end__ |-- all other states ---------------------> fix_node
fix_node .......... LLM (fixer role) | route_fix_result |-- ESCALATED or FAILED ------------------> __end__ |-- no attempts remain -------------------> __end__ |-- all other states ---------------------> build_node
__end__ returns control to supervisor_node in the parent graph.What does each routing function decide?
Section titled “What does each routing function decide?”route_init_to_next sends the run to escalate_node when build_status is FAILED or ESCALATED. A failed clone sets FAILED. When cached_recipe is set, the run goes to cached_recipe_node. Otherwise the run goes to analyst_node.
route_cached_recipe_to_next sends the run to supervisor_node when cached_recipe_node wrote a build_plan. Otherwise the run goes to analyst_node and plans as usual.
route_analyst_to_next sends the run to escalate_node when package_analysis is None. The analyst always writes an analysis, from the LLM or from its deterministic fallback, so None means the node crashed. Otherwise the run goes to heuristic_plan_node.
route_heuristic_plan_to_next sends the run to supervisor_node when a build_plan exists. Otherwise the run goes to scout_node.
route_supervisor_to_next makes no LLM call. It checks these conditions in this order and returns the first match:
build_statusisSUCCESS: go tofinish_node. This is true also at the attempt or cost limit.build_statusisESCALATED: go toescalate_node.should_escalate(state)returns true: go toescalate_node.attempt_countismax_attemptsor more: go toescalate_node.attempt_countis 3 or more and the last 3 errors have one signature: go toescalate_node.package_analysisisNone: go toanalyst_node.build_statusisFAILEDand a new plan is needed: go toscout_node.build_statusisFAILED: go tobuild_fix_subgraph.- A
build_planexists andbuild_statusisPENDINGorBUILDING: go tobuild_fix_subgraph. - In all other states: go to
scout_node.
A new plan is needed when the last error category is DEPENDENCY or MISSING_TOOLS. A new plan is also needed one time for each of 6 error signatures, for example unable to locate package or ./configure: no such file or directory. The recovery heuristics page lists all 6.
route_finish_to_next sends the run to optimize_node when optimize_requested is true, which only --optimize sets. Otherwise the run ends. optimize_node itself skips a port that is not verified, so the report always gives the reason.
route_build_result sends a SUCCESS build to verify_node. It ends the subgraph on ESCALATED or when attempt_count reaches max_attempts. Otherwise it sends the run to fix_node.
route_verify_result ends the subgraph on SUCCESS or ESCALATED, or when no attempts remain. It also ends the subgraph when a fix exists and the last error category is not LINKING, COMPILATION or ARCHITECTURE. Otherwise it sends the run to fix_node. verify_node sets FAILED only for a wrong-architecture verdict, with category ARCHITECTURE, so that verdict goes to the fixer while attempts remain.
route_fix_result ends the subgraph on ESCALATED or FAILED, or when no attempts remain. Otherwise it sends the run to build_node again.
Which plan does the build use?
Section titled “Which plan does the build use?”heuristic_plan_node writes a default plan with no LLM call. It does this only for cmake, make, autotools and go, and only when all of these conditions are true:
- The analyst did not set
needs_custom_planin an LLM-grounded analysis. - The architecture scan found no
criticalfinding and fewer than 3highfindings. - The build-system confidence is 0.5 or more.
The analyst’s build system can replace the detected one. This happens when the analysis is LLM-grounded, the analyst type is not unknown, and its confidence is the same or higher. The default plan installs the dependencies that the analyst and the scripted detection found, if each name is in the platform package map. In all other cases the LLM scout writes the plan. If the scout fails, create_fallback_build_plan() writes a fixed plan for the detected build system.
With --optimize and without --force, a verified recipe cache entry replaces the planning steps. cached_recipe_node does these steps:
- It builds the
BuildPlanfrom the phases and commands of the cached entry. - It writes a
PackageAnalysiswithllm_grounded=False. The expected artifacts come from the cached artifact names. - It applies the cached patches. A patch that does not apply is only a warning, and the build decides.
- It logs a note when the current commit is not the commit that the entry verified.
The analyst and the scout make no LLM call in this path. A failed build still goes to the fixer and the normal re-plan loop. When the cached entry has no command, the node clears cached_recipe and the run plans as usual.
How do nodes handle exceptions?
Section titled “How do nodes handle exceptions?”Every node function has the @agent_node(AgentRole.X) decorator. The decorator does three things:
- It sets
state.current_agentto the role. - When the exception text looks like a rate limit, for example
429,quota exceededor402, it runs the node again. A node runs 3 times at most. The wait before a new run is30 * 2**attemptseconds plus up to 15 seconds of random jitter. - For any other exception, or a third rate-limit failure, it adds an error record, sets
build_statustoESCALATED, and returns the state.
The routing functions then send an ESCALATED run to escalate_node. The run does not return to the scout or the fixer, because an LLM cannot repair an infrastructure fault.
optimize_node is the exception to this rule. It catches every exception itself and writes it to state.optimization with the status error. A performance mode failure therefore never escalates a finished port, and the rate-limit retry never runs the measurements a second time.
What happens at the finish?
Section titled “What happens at the finish?”supervisor_node routes every SUCCESS to finish_node, verified or not. finish_node does these steps:
- It sets
build_statustoSUCCESS. - It runs the package’s own tests one time. The result never changes the build status.
- It asks the summarizer model for the recipe. If that call fails, it writes a short fallback recipe with the build steps and the verification verdict.
- It ranks the build outputs with the artifact curator.
- If the build is verified, it saves few-shot examples and a recipe cache entry. If the build is not verified, it saves nothing.
With --optimize, route_finish_to_next then sends the run to optimize_node. See Performance mode internals.
main.py then writes the outputs and exits with 0 for a verified port, or 3 when no riscv64 output is proven. With --package, a packaging failure gives exit code 2. Performance mode never changes the exit code. See Verification for the verdicts.
What happens at an escalation?
Section titled “What happens at an escalation?”escalate_node makes no LLM call. It sets build_status to ESCALATED and writes an escalation report into context_cache["escalation_report"]. The report has these parts:
- A summary: repository, status, attempts, cost and duration.
- The last error, with its category and severity.
- The last 5 fix attempts.
- Up to 5
highorcriticalarchitecture findings. - A recommendation from
should_escalate(). - The last 5 error records.
main.py then prints the last error and the last 10 audit events, and exits with 1. See Exit codes.