Package dyntabs.ai

Class Flow

java.lang.Object
dyntabs.ai.Flow

public final class Flow extends Object
A deterministic, developer-authored pipeline: it runs a fixed sequence of named steps, threading a typed 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

  1. Correctness — the money/state path runs in the same order every time.
  2. Testability — mock the LLM at the edges and assert invariants ("stock is checked before pay"); each FlowStep is a pure function of its FlowContext.
  3. Safety — a prompt-injection cannot reorder or invent steps; the model does not drive.
  4. Cost/latency — two model calls at the edges, not six in a loop.
  5. 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: