# 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](#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.

```java
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.

```java
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.

```java
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.

```java
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.

```java
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.

```java
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.

```java
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.

```java
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 `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

| 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 |

## Related

- [Planning](planning.md): goal planners and plan execution.
- [Context](context.md): compaction and memory consolidation.
- [Learning](learning.md): norms and refinement loops.
