What is a Reasoning Engine?
A Reasoning Engine is the “brain” of an agent. It controls how the agent thinks, when it calls tools, and how it arrives at a final answer.
Every engine implements a single method:
// Source: packages/core/src/types/index.ts:206-209
export interface ReasoningEngine < TData = unknown > {
readonly name : string
execute ( rCtx : ReasoningContext < TData >) : Promise < string >
}
The engine receives a ReasoningContext (full runtime environment) and returns the final output string.
ReasoningContext
Engines interact with the agent through a rich context object:
// Source: packages/core/src/types/index.ts:215-230
export interface ReasoningContext < TData = unknown > {
/** The enclosing execution context */
ctx : ExecutionContext < TData >
/** The configured model provider */
model : ModelProvider
/** All registered tools */
tools : ToolRegistry
/** Agent policy constraints */
policy : AgentPolicy
/** System prompt (if set) */
systemPrompt ?: string | undefined
/** Append a step to state.steps and emit a 'step:reasoning' event */
pushStep ( step : ReasoningStep ) : void
/** Execute a tool by name and record the result */
callTool ( name : string , args : Record < string , unknown >, callId : string ) : Promise < unknown >
}
Key Methods
Records a reasoning step for observability. Every call:
Appends to ctx.state.steps
Emits a step:reasoning event
Source: packages/core/src/reasoning/context.ts:45-48 rCtx . pushStep ({
type: 'thought' ,
content: 'Analyzing the user request...' ,
engine: 'react'
})
Built-in Reasoning Engines
AgentLIB ships with five reasoning engines, each optimized for different use cases.
ReAct (Default)
Reason + Act — The canonical agent loop.
import { ReactEngine } from '@agentlib/reasoning'
agent . reasoning ( new ReactEngine ({ maxSteps: 10 }))
// Or use the string shorthand
agent . reasoning ( 'react' )
How it Works
Model thinks and optionally calls tools
Tool results are appended to the conversation
Model thinks again, incorporating results
Repeat until no tool calls → final response
// Source: packages/reasoning/src/engines/react.ts:39-74
async execute ( rCtx : ReasoningContext < TData > ): Promise < string > {
const { ctx } = rCtx
let steps = 0
while ( steps < this.maxSteps) {
const response = await callModel ( rCtx , ctx . state . messages )
ctx . state . messages . push ( response . message )
// Model has thoughts but no tools → emit thought step and continue
if ( response . message . content && response . toolCalls ?. length ) {
const thoughtStep : ThoughtStep = {
type: 'thought' ,
content: response . message . content ,
engine: this . name ,
}
rCtx . pushStep ( thoughtStep )
}
// No tool calls → done
if ( ! response . toolCalls ?. length ) {
const responseStep : ResponseStep = {
type: 'response' ,
content: response . message . content ,
engine: this . name ,
}
rCtx . pushStep ( responseStep )
return response . message . content
}
// Execute all tool calls
await executeToolCalls ( rCtx , response )
steps ++
}
throw new Error ( `[ReactEngine] Max steps ( ${ this . maxSteps } ) reached without a final answer.` )
}
When to Use
General-purpose agents — Works for most tasks
Tool-heavy workflows — Naturally integrates tool calls
Interactive assistants — Balances speed and capability
ReAct is the default engine if you don’t specify one. Source: packages/core/src/agent/agent.ts:163-164
Chain of Thought (CoT)
Explicit step-by-step reasoning before answering.
import { ChainOfThoughtEngine } from '@agentlib/reasoning'
agent . reasoning ( new ChainOfThoughtEngine ({
useThinkingTags: true ,
maxToolSteps: 5
}))
How it Works
Injects a reasoning instruction into the system prompt
Model produces <thinking>...</thinking> + answer
Extracts and emits the thinking as a ThoughtStep
If tool calls: executes them and loops
Returns clean final answer
// Source: packages/reasoning/src/engines/cot.ts:114-131
private _injectInstruction ( messages : ModelMessage []): ModelMessage [] {
if ( ! this . useThinkingTags ) return messages
const result = [ ... messages ]
const systemIdx = result . findIndex (( m ) => m . role === 'system' )
if ( systemIdx >= 0 ) {
const sys = result [ systemIdx ] !
result [ systemIdx ] = {
... sys ,
content: ` ${ sys . content } \n\n ${ this . thinkingInstruction } ` ,
}
} else {
result . unshift ({ role: 'system' , content: this . thinkingInstruction })
}
return result
}
Default Instruction
// Source: packages/reasoning/src/engines/cot.ts:30-32
const DEFAULT_THINKING_INSTRUCTION = `Before answering, reason step by step inside <thinking> tags.
Work through the problem carefully, considering all relevant information.
Then provide your final answer outside the tags.`
When to Use
Math and logic problems — Benefits from explicit reasoning
Complex analysis — Reduces errors through structured thinking
Debugging tasks — Makes reasoning process transparent
Planner
Plan-then-execute — Break down complex tasks into subtasks.
import { PlannerEngine } from '@agentlib/reasoning'
agent . reasoning ( new PlannerEngine ({
maxExecutionSteps: 20 ,
allowReplan: false
}))
How it Works
Phase 1: Planning
Model receives available tools and user request
Produces a JSON array of subtasks with dependencies
Phase 2: Execution
Execute each task sequentially (respecting dependencies)
Each task can call tools
Tasks can access results from prior tasks
Phase 3: Synthesis
Combine task results into a final answer
// Source: packages/reasoning/src/engines/planner.ts:84-143
async execute ( rCtx : ReasoningContext < TData > ): Promise < string > {
const { ctx } = rCtx
// ── Phase 1: Planning ──
const plan = await this . _makePlan ( rCtx )
const planStep: PlanStep = {
type: 'plan' ,
tasks: plan ,
engine: this . name ,
}
rCtx . pushStep ( planStep )
// ── Phase 2: Execution ──
const taskResults = new Map < string , string >()
let executionSteps = 0
for ( const task of plan ) {
if ( executionSteps > = this . maxExecutionSteps ) {
throw new Error ( `[PlannerEngine] Max execution steps ( ${ this . maxExecutionSteps } ) reached.` )
}
// Check dependencies are done
const unmetDeps = ( task . dependsOn ?? []). filter (( dep ) => ! taskResults . has ( dep ))
if (unmetDeps.length) {
continue
}
task. status = 'in_progress'
const thoughtStep: ThoughtStep = {
type: 'thought' ,
content: `Executing task [ ${ task . id } ]: ${ task . description } ` ,
engine: this . name ,
}
rCtx . pushStep ( thoughtStep )
try {
const result = await this . _executeTask ( rCtx , task , taskResults )
task. status = 'done'
task. result = result
taskResults.set(task. id , String ( result ))
executionSteps++
} catch ( err ) {
task. status = 'failed'
if (!this.allowReplan) {
throw new Error (
`[PlannerEngine] Task " ${ task . id } " failed: ${ err instanceof Error ? err . message : String ( err ) } ` ,
)
}
}
}
// ── Phase 3: Synthesize results ──
const summary = await this . _synthesize ( rCtx , plan , taskResults )
rCtx . pushStep ({ type: 'response' , content: summary , engine: this . name })
return summary
}
Plan Structure
// Source: packages/core/src/types/index.ts:159-166
export interface PlanTask {
id : string
description : string
dependsOn ?: string []
status : 'pending' | 'in_progress' | 'done' | 'failed'
result ?: unknown
}
When to Use
Multi-step workflows — Research, data pipeline, report generation
Complex goals — Tasks that naturally decompose into subtasks
Parallel-friendly work — Tasks with clear dependency graphs
Reflect
Generate, critique, revise — Self-improving answers.
import { ReflectEngine } from '@agentlib/reasoning'
agent . reasoning ( new ReflectEngine ({
maxReflections: 3 ,
acceptanceThreshold: 9
}))
How it Works
Generate initial answer (with tool access)
Critique the answer using a separate model call
Revise if score < threshold
Repeat up to maxReflections times
// Source: packages/reasoning/src/engines/reflect.ts:79-119
async execute ( rCtx : ReasoningContext < TData > ): Promise < string > {
const { ctx } = rCtx
// ── Phase 1: Generate initial answer ──
let answer = await this . _generateAnswer ( rCtx , ctx . state . messages )
rCtx.pushStep({ type : 'thought' , content : `Initial answer generated.` , engine : this . name })
// ── Phase 2: Reflection loop ──
for ( let i = 0 ; i < this.maxReflections; i ++) {
const critique = await this . _critique ( rCtx , ctx . input , answer )
const reflectionStep : ReflectionStep = {
type: 'reflection' ,
assessment: `Score: ${ critique . score } /10. Issues: ${ critique . issues . join ( '; ' ) } . ${ critique . suggestion } ` ,
needsRevision: critique . needs_revision ,
engine: this . name ,
}
rCtx . pushStep ( reflectionStep )
if ( ! critique . needs_revision || critique . score >= this . acceptanceThreshold ) {
break
}
// ── Phase 3: Revise ──
rCtx . pushStep ({
type: 'thought' ,
content: `Revising answer (attempt ${ i + 1 } / ${ this . maxReflections } )...` ,
engine: this . name ,
})
answer = await this . _revise ( rCtx , ctx . input , answer , critique )
}
const responseStep: ResponseStep = {
type: 'response' ,
content: answer ,
engine: this . name ,
}
rCtx . pushStep ( responseStep )
return answer
}
// Source: packages/reasoning/src/engines/reflect.ts:39-47
const DEFAULT_CRITIQUE_PROMPT = `You are a critical evaluator. Review the answer below and assess its quality.
Respond in this exact JSON format (no markdown):
{
"score": <0-10>,
"issues": ["<issue 1>", "<issue 2>"],
"suggestion": "<one-sentence improvement suggestion>",
"needs_revision": <true|false>
}
Be strict. Score 10 only for perfect answers. Score < 8 if the answer is incomplete, incorrect, or could be substantially improved.`
When to Use
High-stakes outputs — Accuracy matters more than speed
Writing and analysis — Benefits from revision
QA workflows — Self-validation before returning
Reflect engines make 2-4x more model calls than ReAct. Use when quality justifies the cost.
Autonomous
Open-ended agentic loop — Runs until it explicitly finishes.
import { AutonomousEngine } from '@agentlib/reasoning'
agent . reasoning ( new AutonomousEngine ({
maxSteps: 50 ,
finishToolName: 'finish'
}))
How it Works
Automatically injects a finish tool into the schema
Agent loops: think → act → repeat
Agent calls finish tool when done
Result from finish tool becomes the output
// Source: packages/reasoning/src/engines/autonomous.ts:61-153
async execute ( rCtx : ReasoningContext < TData > ): Promise < string > {
const { ctx , tools , policy } = rCtx
// Inject the finish tool into the schema list for this run
const finishSchema = {
name: this . finishToolName ,
description: this . finishToolDescription ,
parameters: {
type: 'object' ,
properties: {
result: {
type: 'string' ,
description: 'Your final answer or output.' ,
},
},
required: [ 'result' ],
},
}
const baseSchemas = tools
.getSchemas()
.filter((t) => tools.isAllowed(t. name , policy.allowedTools))
const allSchemas = [ ... baseSchemas , finishSchema ]
let steps = 0
while ( steps < this.maxSteps) {
const response = await rCtx . model . complete ({
messages: ctx . state . messages ,
tools: allSchemas ,
})
// ... usage tracking ...
ctx . state . messages . push ( response . message )
// Check for finish tool call
if ( response . toolCalls ?. length ) {
const finishCall = response . toolCalls . find (( tc ) => tc . name === this . finishToolName )
if ( finishCall ) {
const result = String (
( finishCall . arguments as { result ?: unknown }). result ?? response . message . content ,
)
rCtx . pushStep ({ type: 'response' , content: result , engine: this . name })
return result
}
// Execute non-finish tool calls
const regularCalls = response . toolCalls . filter (( tc ) => tc . name !== this . finishToolName )
for ( const tc of regularCalls ) {
await rCtx . callTool ( tc . name , tc . arguments , tc . id )
}
} else if ( ! response . toolCalls ?. length ) {
// Model responded without calling any tool — treat as final answer
const answer = extractText ( response . message . content )
rCtx . pushStep ({ type: 'response' , content: answer , engine: this . name })
return answer
}
steps ++
}
throw new Error (
`[AutonomousEngine] Max steps ( ${ this . maxSteps } ) reached. The agent did not call " ${ this . finishToolName } ".` ,
)
}
When to Use
Long-horizon tasks — Research, exploration, automation
Unpredictable workflows — Agent decides when it’s done
Creative tasks — Open-ended brainstorming, writing
Set a high maxSteps (30-50) and monitor token budgets to prevent runaway costs.
Reasoning Steps
Engines emit typed steps for observability:
// Source: packages/core/src/types/index.ts:137-197
export type ReasoningStep =
| ThoughtStep
| PlanStep
| ToolCallStep
| ToolResultStep
| ReflectionStep
| ResponseStep
Step Types
ThoughtStep
PlanStep
ToolCallStep
ToolResultStep
ReflectionStep
ResponseStep
export interface ThoughtStep {
type : 'thought'
content : string
engine : string
}
The agent’s internal reasoning or commentary. export interface PlanStep {
type : 'plan'
tasks : PlanTask []
engine : string
}
A structured task breakdown (from PlannerEngine). export interface ToolCallStep {
type : 'tool_call'
toolName : string
args : Record < string , unknown >
callId : string
engine : string
}
A tool invocation. export interface ToolResultStep {
type : 'tool_result'
toolName : string
callId : string
result : unknown
error ?: string
engine : string
}
The result (or error) from a tool call. export interface ReflectionStep {
type : 'reflection'
assessment : string
needsRevision : boolean
engine : string
}
Self-critique from ReflectEngine. export interface ResponseStep {
type : 'response'
content : string
engine : string
}
The final answer.
Custom Reasoning Engines
You can implement custom reasoning strategies:
import { ReasoningEngine , ReasoningContext } from '@agentlib/core'
class CustomEngine implements ReasoningEngine {
readonly name = 'custom'
async execute ( rCtx : ReasoningContext ) : Promise < string > {
const { ctx , model , tools } = rCtx
// Your custom reasoning logic here
rCtx . pushStep ({
type: 'thought' ,
content: 'Starting custom reasoning...' ,
engine: this . name
})
// Call model
const response = await model . complete ({
messages: ctx . state . messages ,
tools: tools . getSchemas ()
})
// Handle response, call tools, etc.
// ...
return 'final answer'
}
}
// Use it
const agent = createAgent ({ name: 'agent' })
. reasoning ( new CustomEngine ())
Registering Global Engines
Register engines globally to use string shortcuts:
import { registerEngine } from '@agentlib/core'
registerEngine ( 'custom' , () => new CustomEngine ())
// Now you can use:
agent . reasoning ( 'custom' )
Source: packages/core/src/agent/agent.ts:172-177
Engine Selection Strategy
Simple Tasks
Complex Analysis
Multi-step Workflows
High-stakes Outputs
Open-ended Tasks
Use ReAct (default)
Fast, reliable, general-purpose
Good balance of capability and cost
Use Chain of Thought
Math, logic, debugging
Requires explicit reasoning
Use Planner agent . reasoning ( 'planner' )
Research, report generation
Clear task decomposition
Use Reflect agent . reasoning ( 'reflect' )
Critical writing, analysis
Quality over speed
Use Autonomous agent . reasoning ( 'autonomous' )
Long-horizon exploration
Agent decides when done
Best Practices
Set appropriate step limits
// Too low = premature termination
// Too high = runaway costs
agent . reasoning ( new ReactEngine ({ maxSteps: 10 }))
Monitor token usage
agent . policy ({ tokenBudget: 50000 })
agent . on ( 'run:end' , ({ state }) => {
console . log ( `Tokens used: ${ state . usage . totalTokens } ` )
})
Emit steps for observability
// In custom engines
rCtx . pushStep ({ type: 'thought' , content: '...' , engine: this . name })
Use policy constraints
agent . policy ({
maxSteps: 10 ,
timeout: 30000 ,
allowedTools: [ 'search' , 'calculate' ]
})
Fallback to passthrough
If no reasoning engine is configured and @agentlib/reasoning isn’t imported, AgentLIB uses a simple passthrough engine that makes one model call.
Source: packages/core/src/agent/agent.ts:179-195
Next Steps
Agents - Learn about agent configuration
Tools - Understand how engines call tools
Events - Monitor reasoning steps
Middleware - Intercept reasoning phases