Phases
Phases let you group tasks into named workstreams and declare dependencies between them. Independent phases run in parallel; a phase only starts when all its declared predecessors have completed.
Simple Sequential Phases
Section titled “Simple Sequential Phases”The simplest use of phases is to give logical names to sequential stages of work.
Phase research = Phase.builder() .name("research") .task(Task.builder() .description("Search for recent papers on retrieval-augmented generation") .expectedOutput("List of 10 relevant papers with abstracts") .build()) .task(Task.builder() .description("Summarize the key findings from the papers") .expectedOutput("Bullet-point summary of themes and findings") .build()) .build();
Phase writing = Phase.builder() .name("writing") .after(research) // will not start until research completes .task(Task.builder() .description("Write an outline for a blog post on RAG") .expectedOutput("Structured outline with sections and key points") .build()) .task(Task.builder() .description("Write the full blog post from the outline") .expectedOutput("2000-word blog post in Markdown") .build()) .build();
EnsembleOutput output = Ensemble.builder() .chatLanguageModel(llm) .phase(research) .phase(writing) .build() .run();
System.out.println(output.getFinalOutput());Parallel Independent Phases
Section titled “Parallel Independent Phases”When phases do not depend on each other they run concurrently — each in its own virtual thread.
Phase marketResearch = Phase.builder() .name("market-research") .task(Task.of("Research competitor pricing", "Competitor pricing table")) .task(Task.of("Research target demographics", "Demographics summary")) .build();
Phase technicalResearch = Phase.builder() .name("technical-research") .task(Task.of("Assess implementation complexity", "Complexity score and rationale")) .task(Task.of("Identify required integrations", "Integration checklist")) .build();
Phase report = Phase.builder() .name("report") .after(marketResearch, technicalResearch) // waits for both .task(Task.builder() .description("Write a product feasibility report combining market and technical findings") .expectedOutput("Feasibility report with go/no-go recommendation") .context(marketResearch.getTasks().get(0)) // cross-phase context .context(technicalResearch.getTasks().get(0)) .build()) .build();
EnsembleOutput output = Ensemble.builder() .chatLanguageModel(llm) .phase(marketResearch) .phase(technicalResearch) .phase(report) .build() .run();market-research and technical-research start at the same time. report starts as
soon as both of them have finished.
Kitchen Scenario: Parallel Convergent Phases
Section titled “Kitchen Scenario: Parallel Convergent Phases”Three independent workstreams (one per dish) all converge into a final serving phase.
// Each dish is prepared independently and in parallelPhase steak = Phase.builder() .name("steak") .task(Task.of("Prepare steak", "Seasoned and at room temperature")) .task(Task.of("Sear steak", "Medium-rare, rested for 5 minutes")) .task(Task.of("Plate steak", "Plated with garnish and sauce")) .build();
Phase salmon = Phase.builder() .name("salmon") .task(Task.of("Prepare salmon", "Skin removed, seasoned")) .task(Task.of("Cook salmon", "Crispy skin, fully cooked")) .task(Task.of("Plate salmon", "Plated with lemon and herbs")) .build();
Phase pasta = Phase.builder() .name("pasta") .task(Task.of("Boil pasta", "Al dente")) .task(Task.of("Make sauce", "Reduced tomato sauce, seasoned")) .task(Task.of("Plate pasta", "Pasta and sauce combined, topped with basil")) .build();
// Serving only happens once all dishes are readyPhase serve = Phase.builder() .name("serve") .after(steak, salmon, pasta) .task(Task.of("Deliver all plates", "All three dishes delivered simultaneously")) .build();
EnsembleOutput output = Ensemble.builder() .chatLanguageModel(llm) .phase(steak) .phase(salmon) .phase(pasta) .phase(serve) .build() .run();Execution timeline:
t=0 [steak starts] [salmon starts] [pasta starts]t=? [serve starts when last finishes]Per-Phase Workflow Override
Section titled “Per-Phase Workflow Override”Each phase can use a different workflow strategy. For example, gather data in parallel then write sequentially.
Phase dataGathering = Phase.builder() .name("data-gathering") .workflow(Workflow.PARALLEL) // all data tasks run concurrently .task(Task.of("Fetch sales data", "Sales CSV")) .task(Task.of("Fetch inventory data", "Inventory CSV")) .task(Task.of("Fetch customer data", "Customer CSV")) .build();
Phase analysis = Phase.builder() .name("analysis") .workflow(Workflow.SEQUENTIAL) // analysis tasks depend on each other in order .after(dataGathering) .task(Task.of("Merge datasets", "Combined dataset")) .task(Task.of("Compute metrics", "KPI summary")) .task(Task.of("Generate charts description", "Chart descriptions for report")) .build();
EnsembleOutput output = Ensemble.builder() .chatLanguageModel(llm) .phase(dataGathering) .phase(analysis) .build() .run();Diamond Dependency Pattern
Section titled “Diamond Dependency Pattern”A classic DAG: two parallel phases both feed into a single converging phase.
Phase A = Phase.of("A", taskA);Phase B = Phase.of("B", taskB);Phase C = Phase.of("C", taskC);Phase D = Phase.builder().name("D").after(B, C).task(taskD).build();
// [A] (independent, runs in parallel with B and C but has no successors)// [B] --\// +--> [D]// [C] --/
Ensemble.builder() .chatLanguageModel(llm) .phase(A) .phase(B) .phase(C) .phase(D) .build() .run();Phases with Deterministic Tasks
Section titled “Phases with Deterministic Tasks”Phases work with deterministic handler tasks. No LLM is needed for phases composed
entirely of handler tasks.
Phase fetch = Phase.builder() .name("fetch") .task(Task.builder() .description("Fetch live prices") .expectedOutput("JSON price map") .handler(ctx -> ToolResult.success(priceApi.fetchAll())) .build()) .build();
Phase analyse = Phase.builder() .name("analyse") .after(fetch) .task(Task.builder() .description("Identify the top 3 performing assets") .expectedOutput("Ranked list of top 3 assets with rationale") .context(fetch.getTasks().get(0)) .build()) .build();
EnsembleOutput output = Ensemble.builder() .chatLanguageModel(llm) // only needed for the analyse phase .phase(fetch) .phase(analyse) .build() .run();Reading Phase Outputs
Section titled “Reading Phase Outputs”EnsembleOutput provides both a flat list of all task outputs and a phase-keyed map.
EnsembleOutput output = ensemble.run();
// Flat list -- same as before, backward compatibleList<TaskOutput> all = output.getTaskOutputs();
// Phase-keyed map -- newMap<String, List<TaskOutput>> byPhase = output.getPhaseOutputs();
List<TaskOutput> researchOutputs = byPhase.get("research");List<TaskOutput> writingOutputs = byPhase.get("writing");
// Final output is always the last task of the last completed phaseString finalText = output.getFinalOutput();