Skip to main content

Overview

The ReasoningEngine interface is the contract that all reasoning engines must implement. It defines how agents orchestrate model calls, tool usage, and decision-making during execution. Built-in engines include:

Interface Definition

export interface ReasoningEngine<TData = unknown> {
  readonly name: string
  execute(rCtx: ReasoningContext<TData>): Promise<string>
}

Properties

name
string
required
Unique identifier for the engine. Used in step emissions and error messages.

Methods

execute
(rCtx: ReasoningContext<TData>) => Promise<string>
required
Main execution method. Receives a ReasoningContext with full runtime environment and returns the final string output.

ReasoningContext

The ReasoningContext provides engines with everything they need to execute:
export interface ReasoningContext<TData = unknown> {
  ctx: ExecutionContext<TData>        // Enclosing execution context
  model: ModelProvider                // Configured model provider
  tools: ToolRegistry                 // All registered tools
  policy: AgentPolicy                 // Agent policy constraints
  systemPrompt?: string               // System prompt (if set)
  pushStep(step: ReasoningStep): void // Append step and emit event
  callTool(name: string, args: Record<string, unknown>, callId: string): Promise<unknown>
}

Key Context Members

ctx.state.messages: Current conversation history (ModelMessage[]) ctx.input: User’s input for this run ctx.data: User-defined typed state model.complete(): Call the model with messages and tools tools.getSchemas(): Get all available tool schemas pushStep(): Emit reasoning steps for observability callTool(): Execute a tool and record the result

Creating a Custom Engine

Basic Example

Here’s a minimal custom engine that calls the model once:
import type { ReasoningEngine, ReasoningContext, ResponseStep } from '@agentlib/core'

export class SimpleEngine<TData = unknown> implements ReasoningEngine<TData> {
  readonly name = 'simple'

  async execute(rCtx: ReasoningContext<TData>): Promise<string> {
    const { ctx, model, tools, policy } = rCtx

    // Get available tool schemas
    const toolSchemas = tools
      .getSchemas()
      .filter(t => tools.isAllowed(t.name, policy.allowedTools))

    // Call model
    const response = await model.complete({
      messages: ctx.state.messages,
      tools: toolSchemas
    })

    // Append response to messages
    ctx.state.messages.push(response.message)

    // Track token usage
    if (response.usage) {
      ctx.state.usage.promptTokens += response.usage.promptTokens
      ctx.state.usage.completionTokens += response.usage.completionTokens
      ctx.state.usage.totalTokens += response.usage.totalTokens
    }

    // Emit response step
    const step: ResponseStep = {
      type: 'response',
      content: response.message.content,
      engine: this.name
    }
    rCtx.pushStep(step)

    return response.message.content
  }
}

Advanced Example: Custom Loop

Here’s an engine with a custom reasoning loop:
import type { ReasoningEngine, ReasoningContext, ThoughtStep, ResponseStep } from '@agentlib/core'
import { callModel, executeToolCalls } from '@agentlib/reasoning/utils'

export interface CustomEngineConfig {
  maxIterations?: number
  retryOnError?: boolean
}

export class CustomEngine<TData = unknown> implements ReasoningEngine<TData> {
  readonly name = 'custom'
  private maxIterations: number
  private retryOnError: boolean

  constructor(config: CustomEngineConfig = {}) {
    this.maxIterations = config.maxIterations ?? 5
    this.retryOnError = config.retryOnError ?? false
  }

  async execute(rCtx: ReasoningContext<TData>): Promise<string> {
    const { ctx } = rCtx
    let iteration = 0

    while (iteration < this.maxIterations) {
      // Emit thought step
      rCtx.pushStep({
        type: 'thought',
        content: `Starting iteration ${iteration + 1}`,
        engine: this.name
      } satisfies ThoughtStep)

      try {
        // Call model
        const response = await callModel(rCtx, ctx.state.messages)
        ctx.state.messages.push(response.message)

        // No tool calls → done
        if (!response.toolCalls?.length) {
          rCtx.pushStep({
            type: 'response',
            content: response.message.content,
            engine: this.name
          } satisfies ResponseStep)
          return response.message.content
        }

        // Execute tools
        await executeToolCalls(rCtx, response)
        iteration++
      } catch (error) {
        if (!this.retryOnError) throw error
        
        // Add error context to messages and retry
        ctx.state.messages.push({
          role: 'system',
          content: `Error occurred: ${error.message}. Please try a different approach.`
        })
        iteration++
      }
    }

    throw new Error(`[CustomEngine] Max iterations (${this.maxIterations}) reached`)
  }
}

Utility Functions

The @agentlib/reasoning package exports helper utilities:

callModel

import { callModel } from '@agentlib/reasoning/utils'

const response = await callModel(rCtx, messages, { noTools: false })
Handles model calls with automatic:
  • Tool schema filtering by policy
  • Token usage tracking
  • Budget enforcement

executeToolCalls

import { executeToolCalls } from '@agentlib/reasoning/utils'

await executeToolCalls(rCtx, response)
Executes all tool calls from a model response and appends results to messages.

extractText

import { extractText } from '@agentlib/reasoning/utils'

const clean = extractText(response.message.content)
Removes XML tags like <thinking> from content.

Registering Custom Engines

Register your engine to use string aliases:
import { registerEngine } from '@agentlib/core'
import { CustomEngine } from './custom-engine'

registerEngine('custom', () => new CustomEngine())

// Now you can use:
const agent = new Agent({
  name: 'my-agent',
  reasoning: 'custom'
})
Or use the instance directly:
const agent = new Agent({
  name: 'my-agent',
  reasoning: new CustomEngine({ maxIterations: 10 })
})

Step Types

Engines can emit various step types via rCtx.pushStep():

ThoughtStep

{ type: 'thought', content: string, engine: string }

ResponseStep

{ type: 'response', content: string, engine: string }

PlanStep

{ type: 'plan', tasks: PlanTask[], engine: string }

ReflectionStep

{ type: 'reflection', assessment: string, needsRevision: boolean, engine: string }

ToolCallStep

{ type: 'tool_call', toolName: string, args: Record<string, unknown>, callId: string, engine: string }

ToolResultStep

{ type: 'tool_result', toolName: string, callId: string, result: unknown, error?: string, engine: string }

Best Practices

  1. Always track token usage: Increment ctx.state.usage after model calls
  2. Respect policy constraints: Check policy.tokenBudget, policy.allowedTools, etc.
  3. Emit steps for observability: Use pushStep() liberally for debugging
  4. Handle errors gracefully: Provide clear error messages with engine name
  5. Append messages to ctx.state.messages: Keep conversation history up to date
  6. Use utility functions: Leverage callModel and executeToolCalls to reduce boilerplate

Implementation References

  • ReactEngine: packages/reasoning/src/engines/react.ts:31
  • ChainOfThoughtEngine: packages/reasoning/src/engines/cot.ts:53
  • PlannerEngine: packages/reasoning/src/engines/planner.ts:70
  • ReflectEngine: packages/reasoning/src/engines/reflect.ts:65
  • AutonomousEngine: packages/reasoning/src/engines/autonomous.ts:49
  • Types: packages/core/src/types/index.ts:206

Build docs developers (and LLMs) love