Skip to content
tnsaiDocs
GitHub

Reasoning

tnsai-intelligence has executors that spend several LLM calls on one question: they act in steps, explore branches, vote across samples or split the question into parts. Each one takes an LLMClient and returns a result object you can inspect.

ExecutorPackageWhat it does
ReActExecutorcom.tnsai.reasoning.reactThought, action, observation loop with your action handler
TreeOfThoughtsExecutorcom.tnsai.reasoningGenerates and scores branches, keeps the best
GraphOfThoughtsExecutorcom.tnsai.reasoning.gotLike a tree, plus refinement and merging of nodes
SelfConsistencyExecutorcom.tnsai.reasoningSamples several answers and aggregates them
AotProcessorcom.tnsai.prompt.aotSplits a question into atoms, solves them, combines the answers
CausalReasonercom.tnsai.reasoning.causalOne prompt for a why, what-if or intervention question
ExtendedThinkingClientcom.tnsai.reasoningAsks for separate thinking and answer sections

To run one of the first four inside an agent's chat, see Use a strategy in agent chat.

ReAct

ReActExecutor asks the model for a thought and either an action or a final answer. It passes each action to your ActionHandler and feeds the result back as the observation.

import com.tnsai.reasoning.react.ReActExecutor;
import com.tnsai.reasoning.react.ReActResult;
import com.tnsai.reasoning.react.ReActStep;
import com.tnsai.reasoning.react.StopCondition;
import java.time.Duration;
import java.util.Optional;

ReActExecutor react = ReActExecutor.builder()
    .llm(client)
    .actionHandler((name, input) -> switch (name) {
        case "search" -> Optional.of(searchService.search(input));
        default -> Optional.empty();
    })
    .availableActions("search: look up a fact on the web")
    .maxSteps(8)
    .stopCondition(StopCondition.noActionFor(3))
    .timeout(Duration.ofMinutes(2))
    .build();

ReActResult result = react.execute("Which is larger, Tokyo or New York by population?");

if (result.isSuccess()) {
    System.out.println(result.getFinalAnswer());
}
for (ReActStep step : result.getSteps()) {
    System.out.println(step.format());
}
BuilderDefaultMeaning
llm(LLMClient)RequiredThe model
actionHandler(ActionHandler)RequiredRuns an action. Return Optional.empty() when the action is unknown.
availableActions(String)"No specific actions listed"The action list shown to the model
maxSteps(int)10Step limit, at least 1
thoughtFormat(ThoughtFormat)STRUCTUREDSTRUCTURED or FREE_FORM
stopCondition(StopCondition)NoneExtra stop check after each step
timeout(Duration)5 minutesChecked at the start of each step
systemPrompt(String)Built-in ReAct promptReplace the prompt

ActionHandler.withFallback(primary, message) wraps a handler so an unknown action returns your message.

Stop conditions: StopCondition.finalAnswerDetected(), answerContains(text), noActionFor(n) and anyOf(...).

ReAct result

ReActStatusWhen
SUCCESSThe model wrote Final Answer:
STOPPEDThe stop condition returned true
MAX_STEPS_REACHEDThe step limit was reached first
TIMEOUTThe timeout passed before a step started
ERRORA call threw

ReActResult has getFinalAnswer(), getSteps(), getStepCount(), getStatus(), isSuccess(), getDuration(), getTotalLLMCalls() and getReasoningTrace(). Each ReActStep has getStepNumber(), getThought(), getAction(), getActionInput() and getObservation() (the last three are Optional), plus format().

Tree of Thoughts

TreeOfThoughtsExecutor asks the model for branchingFactor next thoughts per node, scores each one with a BranchEvaluator, and keeps the best per round. At maxDepth, or when the time runs out, it asks the model for a final answer on each remaining leaf.

import com.tnsai.reasoning.BranchEvaluator;
import com.tnsai.reasoning.PruningStrategy;
import com.tnsai.reasoning.ToTResult;
import com.tnsai.reasoning.TreeOfThoughtsExecutor;

TreeOfThoughtsExecutor tot = TreeOfThoughtsExecutor.builder()
    .llm(client)
    .evaluator(BranchEvaluator.llm(evalClient))
    .pruning(PruningStrategy.BEAM_SEARCH)
    .beamWidth(3)
    .maxDepth(4)
    .build();

ToTResult result = tot.explore("Design a REST API for a todo app");

result.getBestLeaf().ifPresent(leaf -> System.out.println(leaf.getFinalAnswer()));
System.out.println(result.getBestPath());
BuilderDefaultMeaning
llm(LLMClient)RequiredGenerates thoughts and final answers
evaluator(BranchEvaluator)RequiredScores each new node from 0 to 1
pruning(PruningStrategy)BEAM_SEARCHSee below
beamWidth(int)3Nodes kept per round, at least 1
maxDepth(int)5Depth at which a node becomes a leaf, at least 1
branchingFactor(int)3Thoughts requested per node, at least 1
pruneThreshold(double)0.3Nodes scoring below this are pruned
timeout(Duration)10 minutesChecked before each round

Pruning strategies

PruningStrategyBehaviour
BEAM_SEARCHPrunes below the threshold, keeps the top beamWidth by score
BEST_FIRSTPrunes below the threshold, keeps the top beamWidth by cumulative score
GREEDYPrunes below the threshold, keeps the single best
DEPTH_LIMITEDNo score pruning; keeps every node up to maxDepth
EXHAUSTIVENo pruning at all
MCTSNot implemented: behaves as threshold pruning with no frontier limit

Evaluators

BranchEvaluatorScore
llm(client)The model rates the path from 0 to 100; a failed call scores 0.5
heuristic()Keyword and length rules, with no LLM call
combined(evaluators...)The average of the given evaluators

A BranchEvaluator is a single method, evaluate(Evaluable node, String goal), so a lambda works too.

Tree result

ToTResultReturns
getBestLeaf()Optional<ThoughtNode>: the leaf with the highest average score
getBestPath()That leaf's thoughts joined with " -> "
getBestScore()That leaf's average score along its path
hasSolution()Whether the best leaf's own score is above 0.5
getTopPaths(k)The k leaves with the highest average score
getAllLeaves(), getRoot()The tree
getTotalNodes(), getPrunedNodes(), getMaxDepthReached(), getDuration()Statistics

ThoughtNode has getThought(), getScore(), getDepth(), getParent(), getChildren(), getPath(), getThoughtChain(), getCumulativeScore(), getAverageScore() and getFinalAnswer().

Graph of Thoughts

GraphOfThoughtsExecutor grows a graph in rounds. Each round it generates children for the frontier, refines nodes that scored above 0.3, and merges the round's best new nodes into one.

import com.tnsai.reasoning.BranchEvaluator;
import com.tnsai.reasoning.got.GoTOperation;
import com.tnsai.reasoning.got.GoTResult;
import com.tnsai.reasoning.got.GraphOfThoughtsExecutor;
import java.util.List;

GraphOfThoughtsExecutor got = GraphOfThoughtsExecutor.builder()
    .llm(client)
    .evaluator(BranchEvaluator.llm(client))
    .operations(List.of(GoTOperation.GENERATE, GoTOperation.REFINE, GoTOperation.AGGREGATE))
    .maxNodes(20)
    .build();

GoTResult result = got.explore("Design a database schema for e-commerce");
System.out.println(result.getBestThought());
System.out.println(result.mergeCount());
BuilderDefaultMeaning
llm(LLMClient)RequiredThe model
evaluator(BranchEvaluator)RequiredScores every new node
operations(List<GoTOperation>)GENERATE, AGGREGATE, REFINE, SCOREWhich steps run each round
maxNodes(int)20Stop when the graph has this many nodes
branchingFactor(int)3Children per node, and frontier size
timeout(Duration)10 minutesChecked before each round
GoTOperationEffect
GENERATENew child thoughts for each frontier node
REFINEAn improved version of each frontier node scoring above 0.3
AGGREGATEMerges the top 3 new nodes of the round, when there are at least 2
SCORENo effect: every new node is scored whether it is listed or not

GoTResult is a record: root(), bestNode(), allNodes(), aggregatedInsight() (the best node's thought), totalNodes(), mergeCount() and duration(), plus getBestThought(), getBestScore() and hasSolution() (best score above 0.5). A GoTNode with more than one parent is a merge node: isMergeNode(), getParents().

Self-consistency

SelfConsistencyExecutor asks the same question numPaths times at slightly different temperatures and aggregates the answers.

import com.tnsai.reasoning.Aggregation;
import com.tnsai.reasoning.ConsistencyResult;
import com.tnsai.reasoning.SelfConsistencyExecutor;

SelfConsistencyExecutor executor = SelfConsistencyExecutor.builder()
    .llm(client)
    .numPaths(5)
    .aggregation(Aggregation.MAJORITY_VOTE)
    .build();

ConsistencyResult result = executor.reason("What is 17 * 23?");
System.out.println(result.getConsensusAnswer());
System.out.println(result.getConfidence());
System.out.println(result.getAnswerCounts());
BuilderDefaultMeaning
llm(LLMClient)RequiredThe model
numPaths(int)5Samples, at least 1
aggregation(Aggregation)MAJORITY_VOTESee below
baseTemperature(double)0.7Temperature of the first sample
temperatureVariance(double)0.1Step between sample temperatures, which alternate above and below the base, kept within 0.1 to 2.0
parallel(boolean)trueRun samples concurrently
maxConcurrency(int)5Concurrent samples, above 0
timeout(Duration)5 minutesLimit for parallel runs
systemPrompt(String)NoneOptional system prompt
AggregationAnswer and confidence
MAJORITY_VOTEMost frequent answer; share of samples that gave it
WEIGHTED_VOTEVotes weighted by the length of each sample's reasoning
UNANIMOUSAn answer with confidence 1.0 only when every sample agrees
THRESHOLDAn answer only when more than half the samples gave it
LLM_SYNTHESISThe model writes a final answer from all samples; confidence is fixed at 0.8

ConsistencyResult has getConsensusAnswer(), getConfidence(), getAnswerCounts(), getAllPaths(), getTotalPaths(), isUnanimous() and isConfident(threshold).

Atom of Thought

AotProcessor splits a question into atoms, solves them in dependency order and combines the answers. Questions that score below the complexity threshold are answered as a single atom.

import com.tnsai.prompt.aot.AotProcessor;
import com.tnsai.prompt.aot.AotResult;

AotProcessor aot = AotProcessor.builder()
    .llmClient(client)
    .maxAtoms(8)
    .build();

try {
    AotResult result = aot.process("Compare the 3-year cost of renting and buying a $300k home at 6%");
    System.out.println(result.getAnswer());
    System.out.println(result.getConfidence());
    System.out.println(result.getDetailedTrace());
} finally {
    aot.shutdown();
}

new AotProcessor(client) uses the defaults.

BuilderDefaultMeaning
llmClient(LLMClient)Required unless you pass all three stagesThe model
complexityThreshold(int)50Below this score the question is one atom
maxAtoms(int)10Atom limit
minConfidence(float)30Solved atoms below this confidence (0 to 100) are left out of the synthesis
synthesisStrategy(...)WEIGHTED_COMBINATIONAtomSynthesizer strategy: WEIGHTED_COMBINATION, LLM_SYNTHESIS, HIGHEST_CONFIDENCE or CONCATENATE
generator, solver, synthesizerBuilt from the clientReplace a stage. maxAtoms and minConfidence apply only to the stages built for you.

processForced(question) skips the complexity check. A blank question returns an empty result, and an exception returns a failed one: check isSuccess() and getErrorMessage().

AotResult also has getAtoms(), getAtomCount(), getSolvedAtomCount(), getAverageAtomConfidence(), isConfident(threshold), getReasoning() and getSummary(). Confidence is on a 0 to 100 scale.

Call shutdown() when you are done: the solver owns a thread pool.

Causal questions

CausalReasoner sends one prompt per question, with your causal model and context, and parses the answer, reasoning and confidence from the reply.

import com.tnsai.reasoning.causal.CausalReasoner;
import com.tnsai.reasoning.causal.CausalResult;
import java.util.Map;

CausalReasoner reasoner = CausalReasoner.builder()
    .llm(client)
    .causalModel("Sales depend on marketing spend, season and competitor pricing")
    .build();

Map<String, Object> context = Map.of("q3_sales", 150_000, "marketing_budget", 50_000);

CausalResult why = reasoner.why("Why did sales drop in Q3?", context);
CausalResult whatIf = reasoner.whatIf("What if we doubled marketing?", context);
CausalResult effect = reasoner.intervene("Cut prices by 15%", context);

CausalResult is a record: queryType() (WHY, WHAT_IF or INTERVENTION), answer(), reasoning() and confidence() from 0 to 1. When the reply states no confidence, it is 0.5. When the call fails, the answer starts with Error: and confidence is 0.

Separate thinking from the answer

ExtendedThinkingClient asks the model to reason inside <thinking> tags and answer inside <answer> tags, and splits the reply into a ThinkingResult. It works through the prompt, so any LLMClient can be used.

import com.tnsai.reasoning.ExtendedThinkingClient;
import com.tnsai.reasoning.ThinkingResult;

ExtendedThinkingClient thinker = ExtendedThinkingClient.builder()
    .anthropic(client)
    .thinkingBudget(8_000)
    .build();

ThinkingResult result = thinker.think("Is 2^61 - 1 prime?");
System.out.println(result.getFinalAnswer());
System.out.println(result.getThinkingProcess());
BuilderDefaultMeaning
anthropic(LLMClient)RequiredThe client to call
thinkingBudget(int)10000Token budget stated in the prompt, at least 100

think(problem, Optional<String> systemPrompt) adds your system prompt, and thinkWithHistory(problem, history) passes earlier turns. think returns a result whose answer starts with Extended thinking failed: when the call throws; thinkWithHistory throws.

ThinkingResult has getThinkingProcess(), getFinalAnswer(), getThinkingBlocks(), hasThinking(), getThinkingTokens(), getOutputTokens() and getTotalTokens(). The token counts are estimates (characters divided by 4). Without an <answer> tag, the answer is the reply with the thinking removed.

Use a strategy in agent chat

An agent can route every chat message through ReAct, Tree of Thoughts, Graph of Thoughts or self-consistency. tnsai-intelligence must be on the classpath.

Agent agent = AgentBuilder.create()
    .llm(client)
    // role and the other settings as usual
    .reasoningStrategy("self-consistency")
    .build();

String answer = agent.chat("What is 17 * 23?");
agent.getLastReasoningResult().ifPresent(r -> System.out.println(r.metadata()));
NameRuns
reactReActExecutor, 10 steps
totTreeOfThoughtsExecutor, LLM evaluator, depth 4, beam 3
gotGraphOfThoughtsExecutor, GENERATE, AGGREGATE, REFINE, 15 nodes
self-consistencySelfConsistencyExecutor, 5 samples, majority vote
  • The strategy gets the grounded message only, not the conversation history.
  • On the react path, actions are not run: every observation is [Tool execution delegated to Agent]. To run actions, use ReActExecutor directly with your own ActionHandler.
  • If the name cannot be resolved, for example because tnsai-intelligence is missing, the agent chats without a strategy and does not report it.
  • reasoningStrategy(ReasoningStrategyHandle) passes your own handle and wins over a name. agent.setReasoningStrategy(null) turns the strategy off.

Choosing an executor

ProblemExecutorLLM calls
Needs tools or lookups between stepsReActExecutorOne per step
A short answer you can compare across samplesSelfConsistencyExecutornumPaths
A design or plan with alternatives at each stepTreeOfThoughtsExecutorPer node: one generation, plus one evaluation per child with the LLM evaluator, plus one final answer per leaf
Alternatives worth combiningGraphOfThoughtsExecutorAs for a tree, plus refinement and merge calls
A question with separable partsAotProcessorAt most one decomposition, one per atom, and one synthesis with LLM_SYNTHESIS
A cause or effect questionCausalReasonerOne
  • Planning: goal planners and plan execution.
  • Context: compaction and memory consolidation.
  • Learning: norms and refinement loops.