Skip to main content

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:
  1. Appends to ctx.state.steps
  2. 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'
})
Executes a tool with full middleware and event support. Handles:
  • Policy validation
  • Error handling
  • Result recording
  • Message appending
Source: packages/core/src/reasoning/context.ts:50-119
const result = await rCtx.callTool('search', { query: 'weather' }, 'call-123')

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

  1. Model thinks and optionally calls tools
  2. Tool results are appended to the conversation
  3. Model thinks again, incorporating results
  4. 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

  1. Injects a reasoning instruction into the system prompt
  2. Model produces <thinking>...</thinking> + answer
  3. Extracts and emits the thinking as a ThoughtStep
  4. If tool calls: executes them and loops
  5. 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

  1. Generate initial answer (with tool access)
  2. Critique the answer using a separate model call
  3. Revise if score < threshold
  4. 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
}

Critique Format

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

  1. Automatically injects a finish tool into the schema
  2. Agent loops: think → act → repeat
  3. Agent calls finish tool when done
  4. 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

export interface ThoughtStep {
  type: 'thought'
  content: string
  engine: string
}
The agent’s internal reasoning or commentary.

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

Use ReAct (default)
agent.reasoning('react')
  • Fast, reliable, general-purpose
  • Good balance of capability and cost

Best Practices

  1. Set appropriate step limits
    // Too low = premature termination
    // Too high = runaway costs
    agent.reasoning(new ReactEngine({ maxSteps: 10 }))
    
  2. Monitor token usage
    agent.policy({ tokenBudget: 50000 })
    
    agent.on('run:end', ({ state }) => {
      console.log(`Tokens used: ${state.usage.totalTokens}`)
    })
    
  3. Emit steps for observability
    // In custom engines
    rCtx.pushStep({ type: 'thought', content: '...', engine: this.name })
    
  4. Use policy constraints
    agent.policy({
      maxSteps: 10,
      timeout: 30000,
      allowedTools: ['search', 'calculate']
    })
    
  5. 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

Build docs developers (and LLMs) love