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.
| Executor | Package | What it does |
|---|---|---|
ReActExecutor | com.tnsai.reasoning.react | Thought, action, observation loop with your action handler |
TreeOfThoughtsExecutor | com.tnsai.reasoning | Generates and scores branches, keeps the best |
GraphOfThoughtsExecutor | com.tnsai.reasoning.got | Like a tree, plus refinement and merging of nodes |
SelfConsistencyExecutor | com.tnsai.reasoning | Samples several answers and aggregates them |
AotProcessor | com.tnsai.prompt.aot | Splits a question into atoms, solves them, combines the answers |
CausalReasoner | com.tnsai.reasoning.causal | One prompt for a why, what-if or intervention question |
ExtendedThinkingClient | com.tnsai.reasoning | Asks 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());
}| Builder | Default | Meaning |
|---|---|---|
llm(LLMClient) | Required | The model |
actionHandler(ActionHandler) | Required | Runs 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) | 10 | Step limit, at least 1 |
thoughtFormat(ThoughtFormat) | STRUCTURED | STRUCTURED or FREE_FORM |
stopCondition(StopCondition) | None | Extra stop check after each step |
timeout(Duration) | 5 minutes | Checked at the start of each step |
systemPrompt(String) | Built-in ReAct prompt | Replace 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
ReActStatus | When |
|---|---|
SUCCESS | The model wrote Final Answer: |
STOPPED | The stop condition returned true |
MAX_STEPS_REACHED | The step limit was reached first |
TIMEOUT | The timeout passed before a step started |
ERROR | A 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());| Builder | Default | Meaning |
|---|---|---|
llm(LLMClient) | Required | Generates thoughts and final answers |
evaluator(BranchEvaluator) | Required | Scores each new node from 0 to 1 |
pruning(PruningStrategy) | BEAM_SEARCH | See below |
beamWidth(int) | 3 | Nodes kept per round, at least 1 |
maxDepth(int) | 5 | Depth at which a node becomes a leaf, at least 1 |
branchingFactor(int) | 3 | Thoughts requested per node, at least 1 |
pruneThreshold(double) | 0.3 | Nodes scoring below this are pruned |
timeout(Duration) | 10 minutes | Checked before each round |
Pruning strategies
PruningStrategy | Behaviour |
|---|---|
BEAM_SEARCH | Prunes below the threshold, keeps the top beamWidth by score |
BEST_FIRST | Prunes below the threshold, keeps the top beamWidth by cumulative score |
GREEDY | Prunes below the threshold, keeps the single best |
DEPTH_LIMITED | No score pruning; keeps every node up to maxDepth |
EXHAUSTIVE | No pruning at all |
MCTS | Not implemented: behaves as threshold pruning with no frontier limit |
Evaluators
BranchEvaluator | Score |
|---|---|
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
ToTResult | Returns |
|---|---|
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());| Builder | Default | Meaning |
|---|---|---|
llm(LLMClient) | Required | The model |
evaluator(BranchEvaluator) | Required | Scores every new node |
operations(List<GoTOperation>) | GENERATE, AGGREGATE, REFINE, SCORE | Which steps run each round |
maxNodes(int) | 20 | Stop when the graph has this many nodes |
branchingFactor(int) | 3 | Children per node, and frontier size |
timeout(Duration) | 10 minutes | Checked before each round |
GoTOperation | Effect |
|---|---|
GENERATE | New child thoughts for each frontier node |
REFINE | An improved version of each frontier node scoring above 0.3 |
AGGREGATE | Merges the top 3 new nodes of the round, when there are at least 2 |
SCORE | No 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());| Builder | Default | Meaning |
|---|---|---|
llm(LLMClient) | Required | The model |
numPaths(int) | 5 | Samples, at least 1 |
aggregation(Aggregation) | MAJORITY_VOTE | See below |
baseTemperature(double) | 0.7 | Temperature of the first sample |
temperatureVariance(double) | 0.1 | Step between sample temperatures, which alternate above and below the base, kept within 0.1 to 2.0 |
parallel(boolean) | true | Run samples concurrently |
maxConcurrency(int) | 5 | Concurrent samples, above 0 |
timeout(Duration) | 5 minutes | Limit for parallel runs |
systemPrompt(String) | None | Optional system prompt |
Aggregation | Answer and confidence |
|---|---|
MAJORITY_VOTE | Most frequent answer; share of samples that gave it |
WEIGHTED_VOTE | Votes weighted by the length of each sample's reasoning |
UNANIMOUS | An answer with confidence 1.0 only when every sample agrees |
THRESHOLD | An answer only when more than half the samples gave it |
LLM_SYNTHESIS | The 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.
| Builder | Default | Meaning |
|---|---|---|
llmClient(LLMClient) | Required unless you pass all three stages | The model |
complexityThreshold(int) | 50 | Below this score the question is one atom |
maxAtoms(int) | 10 | Atom limit |
minConfidence(float) | 30 | Solved atoms below this confidence (0 to 100) are left out of the synthesis |
synthesisStrategy(...) | WEIGHTED_COMBINATION | AtomSynthesizer strategy: WEIGHTED_COMBINATION, LLM_SYNTHESIS, HIGHEST_CONFIDENCE or CONCATENATE |
generator, solver, synthesizer | Built from the client | Replace 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());| Builder | Default | Meaning |
|---|---|---|
anthropic(LLMClient) | Required | The client to call |
thinkingBudget(int) | 10000 | Token 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()));| Name | Runs |
|---|---|
react | ReActExecutor, 10 steps |
tot | TreeOfThoughtsExecutor, LLM evaluator, depth 4, beam 3 |
got | GraphOfThoughtsExecutor, GENERATE, AGGREGATE, REFINE, 15 nodes |
self-consistency | SelfConsistencyExecutor, 5 samples, majority vote |
- The strategy gets the grounded message only, not the conversation history.
- On the
reactpath, actions are not run: every observation is[Tool execution delegated to Agent]. To run actions, useReActExecutordirectly with your ownActionHandler. - If the name cannot be resolved, for example because
tnsai-intelligenceis 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
| Problem | Executor | LLM calls |
|---|---|---|
| Needs tools or lookups between steps | ReActExecutor | One per step |
| A short answer you can compare across samples | SelfConsistencyExecutor | numPaths |
| A design or plan with alternatives at each step | TreeOfThoughtsExecutor | Per node: one generation, plus one evaluation per child with the LLM evaluator, plus one final answer per leaf |
| Alternatives worth combining | GraphOfThoughtsExecutor | As for a tree, plus refinement and merge calls |
| A question with separable parts | AotProcessor | At most one decomposition, one per atom, and one synthesis with LLM_SYNTHESIS |
| A cause or effect question | CausalReasoner | One |