Class Flow
FlowContext from one to the next, and calls the LLM only where you put a
step that calls it.
What this is, and how it differs from EasyAgent
An EasyAgent hands the whole task to the model and lets it decide which service to
call next and in what order — great when the path is genuinely unknown, but non-deterministic and
hard to unit-test. A Flow is the opposite discipline: you author the order in
plain Java, and the model is invited in only at the edges you declare (typically "understand the
request" at the start and "summarize the outcome" at the end). For a known business process —
check stock, take payment, create order, ship — there is exactly one correct order and it must be
the same every time; a flow pins it down.
Familiar analogy: a recipe followed step by step versus a chef improvising.
agent() is the improvising chef (powerful, unpredictable); flow() is the recipe card —
the same dish, the same order, every service, testable bite by bite. The LLM is the one exotic
ingredient you reach for only at the two moments the recipe calls for it.
FlowContext out = EasyAI.flow()
.step("understand", ctx -> EasyAI.extract(OrderRequest.class).from(ctx.inputText())) // LLM
.step("checkStock", ctx -> inventory.checkStock(ctx.get("understand", OrderRequest.class)))
.step("pay", ctx -> payment.charge(ctx.get("understand", OrderRequest.class)))
.step("createOrder",ctx -> orders.create(ctx.get("understand", OrderRequest.class)))
.step("ship", ctx -> shipping.schedule(ctx.get("createOrder", Order.class)))
.step("summarize", ctx -> EasyAI.chat().build()
.send("Tell the user what happened:\n" + ctx.trail())) // LLM
.withEventListener(e -> log.info("{}", e)) // same live stream as agent() → Activity panel
.build()
.run("Order 3 blue watches, ship home.");
String reply = (String) out.result(); // the "summarize" step's output
What it buys you
- Correctness — the money/state path runs in the same order every time.
- Testability — mock the LLM at the edges and assert invariants ("stock is checked
before pay"); each
FlowStepis a pure function of itsFlowContext. - Safety — a prompt-injection cannot reorder or invent steps; the model does not drive.
- Cost/latency — two model calls at the edges, not six in a loop.
- Explainability — the run is your code and
FlowContext.trail(), not the model's monologue.
Place in the chain
EasyAI.flow() → FlowBuilder.step(...)·step(...) → FlowBuilder.build() → Flow
→ Flow.run(input)
→ for each step: new FlowContext(input, results-so-far) → FlowStep.run(ctx)
→ returns the final FlowContext snapshot
Observability rides the same EasyAIEvent stream every other EasyAI
capability uses (here under EasyAIEvent.Source.FLOW), so the TabForge
demo's Activity panel renders a flow run with no new wiring.
Steps run in registration order. Two declarative refinements keep the branch logic visible at
the flow level (in the trace, the event stream, and tests) instead of buried inside a step:
FlowBuilder.stepIf(String, java.util.function.Predicate, FlowStep) runs a step only when a
guard holds (otherwise it is skipped and stores no result), and
FlowBuilder.orElse(FlowStep) attaches fallback alternatives that are tried, in order, if
the primary step throws. Both are declared, not discovered — there is no planner; you
still author the path. (Simple in-step conditionals with a plain Java if remain perfectly
fine; stepIf is for when you want the branch to be a first-class, named, skippable step.)
- See Also:
-
Method Summary
Modifier and TypeMethodDescriptionRuns every step once, in registration order, threading a typedFlowContextthrough them, and returns the final context snapshot.
-
Method Details
-
run
Runs every step once, in registration order, threading a typedFlowContextthrough them, and returns the final context snapshot.Called by application code (the end of the
EasyAI.flow()...build().run(input)chain). Before each step it builds a freshFlowContextholdinginputplus the results of all steps that have already run; it passes that toFlowStep.run(FlowContext)and stores the returned value under the step's name for later steps. If a step throws, the pipeline stops and the exception is wrapped in aFlowExceptionnaming that step.The run is bracketed with
EasyAIEvent.Phase.STARTED/EasyAIEvent.Phase.FINISHEDevents, with aSTEP_STARTEDbefore and aSTEPafter each step (and anERRORon failure) — the same live streamagent()emits, so any registeredEasyAIListenersees the run unfold in real time.- Parameters:
input- the flow's input, made available to every step viaFlowContext.input()- Returns:
- the final
FlowContextsnapshot — read the outcome withFlowContext.result(), an intermediate value withFlowContext.get(String, Class), or the whole run withFlowContext.trail() - Throws:
FlowException- if any step throws;FlowException.stepName()names the failing step
-