Tutorial 1: Your First Loop¶
In this tutorial you will build the smallest possible Loop Engineering loop, run it, and read the result. No API key required - every component here is plain Python.
What you'll build¶
A loop with a single goal - "count the words in this sentence" - that plans one step, executes it, evaluates the result, and terminates. It is deliberately trivial: the point is to see the six moving parts before you put real work (or an LLM) behind any of them.
The six components¶
LoopEngine does not know how to plan, act, observe, evaluate, recover, or
decide when to stop. You provide those as components, registered against a
ComponentType:
| Component | Interface | Job |
|---|---|---|
PLANNER |
create_plan, revise_plan |
Turn a goal into Steps |
ACTOR |
execute, execute_direct |
Do the actual work of a step |
OBSERVER |
observe |
Turn a raw result into an Observation |
EVALUATOR |
evaluate |
Decide whether the step's result is good enough |
TERMINATOR |
should_terminate |
Decide whether the loop is done |
RECOVERY (optional) |
recover |
Choose a strategy when something fails |
loop_engine.components ships ready-to-use implementations for most of
these (SimplePlanner, SimpleObserver, SimpleTerminator, LLMPlanner,
LLMEvaluator, ...). The only thing genuinely specific to your problem is
usually the Actor - and often the Evaluator, since "good enough" is a
domain judgment call.
Step 1: Write your Actor¶
This is the one piece of domain logic this tutorial needs:
from loop_engine.components import Actor
class WordCountActor(Actor):
"""Domain logic: count words in a step's description."""
async def execute(self, step, observations, context):
return {"step": step.description, "word_count": len(step.description.split())}
async def execute_direct(self, goal, context):
return {"goal": goal, "word_count": len(goal.split())}
Step 2: Write a matching Evaluator¶
SimpleEvaluator (shipped in loop_engine.components) judges a step by
plan progress, which assumes a VERIFIER component is also registered to
promote steps to VERIFIED_COMPLETED. Since this tutorial has no verifier
yet, write the two-line evaluator that actually matches what you're
checking - "did the actor produce a result?":
from loop_engine.types import Evaluation
from loop_engine.components import Evaluator
class HasResultEvaluator(Evaluator):
"""Domain logic: the step passes once the actor produced a result."""
async def evaluate(self, plan, observations, goal, context):
if not observations:
return Evaluation(score=0.0, passed=False, feedback="No result yet")
return Evaluation(score=1.0, passed=True, feedback="Actor produced a result")
Why not just use SimpleEvaluator?
SimpleEvaluator and SimpleTerminator both key off
Plan.get_progress(), which only counts steps in VERIFIED_COMPLETED or
SKIPPED. That promotion normally happens in the verification phase.
With enable_verification=False (below), the engine promotes a passed
step directly - but only once your evaluator has actually said passed.
Writing the two lines above is clearer than reasoning about that
interaction, and it's exactly the kind of two-line domain evaluator
you'll write for real steps too.
Step 3: Wire it into a LoopEngine¶
import asyncio
from loop_engine.core import LoopEngine, LoopConfig
from loop_engine.types import ComponentType, LoopContext, Budget
from loop_engine.components import SimplePlanner, SimpleObserver, SimpleTerminator
async def main():
engine = LoopEngine(LoopConfig(max_iterations=5, enable_verification=False))
engine.register_component(ComponentType.PLANNER, SimplePlanner())
engine.register_component(ComponentType.ACTOR, WordCountActor())
engine.register_component(ComponentType.OBSERVER, SimpleObserver())
engine.register_component(ComponentType.EVALUATOR, HasResultEvaluator())
engine.register_component(ComponentType.TERMINATOR, SimpleTerminator())
context = LoopContext(
goal="Count the words in this sentence",
budget=Budget(max_steps=5),
)
result = await engine.run(context)
print("status:", result.status.name)
print("iterations:", result.iterations)
print("output:", result.output)
print("verification_status:", result.verification_status)
asyncio.run(main())
enable_verification=False tells the engine there's no separate
verification phase in this loop - the evaluator's judgment is final. You'll
add a real verifier in a later tutorial.
Step 4: Run it¶
Real, unedited output:
status: COMPLETED
iterations: 1
output: {'step': 'Count the words in this sentence', 'word_count': 6}
verification_status: True
What just happened¶
engine.run(context) drove the state machine through one full iteration:
- PLANNING -
SimplePlannerturned the goal into a one-stepPlan. - ACTING -
WordCountActor.execute()ran and returned a dict. - OBSERVING -
SimpleObserverwrapped that dict in anObservation. - EVALUATING -
HasResultEvaluatorsaw an observation and passed the step. With verification disabled, the step was promoted straight toVERIFIED_COMPLETED. - ITERATION_COMPLETE -
SimpleTerminatorsawPlan.get_progress() == 1.0and the engine transitioned toCOMPLETED.
Every one of those transitions is validated against an explicit table
(LoopEngine._VALID_TRANSITIONS) - the engine cannot silently skip a phase
or end in an inconsistent state. See the
state machine specification for the
full transition table.
What's next¶
This loop can't fail, so it never needed recovery. Real work fails constantly - APIs time out, generated code doesn't compile, evaluators reject a first draft. Tutorial 2 takes this same shape and adds a step that fails twice before succeeding, so you can watch the engine recover without losing the loop.