Skip to main content

What is an Agent?

An Agent is the central runtime orchestrator in AgentLIB. It coordinates the entire execution flow: loading memory, invoking the reasoning engine, executing tools, running middleware, and emitting events. Think of an agent as a container that brings together:
  • Model Provider - The LLM that powers reasoning
  • Tools - Functions the agent can call
  • Memory - Conversation history persistence
  • Reasoning Engine - The strategy for decision-making
  • Middleware - Lifecycle hooks for cross-cutting concerns
  • Policy - Resource limits and constraints

Creating an Agent

AgentLIB offers two ways to create agents: object-based and class-based.

Object-Based Configuration

The simplest approach uses a configuration object:
import { createAgent, defineTool } from '@agentlib/core'
import { openai } from '@agentlib/providers/openai'

const agent = createAgent({
  name: 'my-assistant',
  description: 'A helpful AI assistant',
  model: openai({ model: 'gpt-4' }),
  systemPrompt: 'You are a helpful assistant that answers concisely.',
  policy: {
    maxSteps: 10,
    timeout: 30000
  }
})

await agent.run('What is the capital of France?')

Class-Based Configuration (Decorators)

For complex agents with many tools, use decorators:
import { Agent, Tool, Arg } from '@agentlib/core'

@Agent({
  name: 'calculator',
  description: 'Math assistant with calculation tools'
})
class CalculatorAgent {
  @Tool('add', 'Add two numbers')
  async add(
    @Arg('a') a: number,
    @Arg('b') b: number
  ): Promise<number> {
    return a + b
  }

  @Tool('multiply', 'Multiply two numbers')
  async multiply(
    @Arg('x') x: number,
    @Arg('y') y: number
  ): Promise<number> {
    return x * y
  }
}

const agent = createAgent(CalculatorAgent)
Decorators require reflect-metadata and TypeScript’s experimentalDecorators enabled.

Agent Lifecycle

When you call agent.run(), the following sequence occurs:
// Source: packages/core/src/agent/agent.ts:78-101
async run(options: RunOptions<TData> | string): Promise<RunResult> {
  const opts: RunOptions<TData> = typeof options === 'string' ? { input: options } : options
  const ctx = createContext<TData>({
    input: opts.input,
    data,
    memory: this._memory,
    emitter: this._emitter,
    sessionId: opts.sessionId,
    signal: opts.signal,
  })

  await this._emitter.emit('run:start', { input: opts.input, sessionId: ctx.sessionId })

  try {
    await this._middleware.run({ scope: 'run:before', ctx })
    const output = await this._executeWithEngine(ctx)
    ctx.state.finishedAt = new Date()
    await this._middleware.run({ scope: 'run:after', ctx })
    await this._emitter.emit('run:end', { output, state: ctx.state })
    return { output, state: ctx.state }
  } catch (err) {
    await this._emitter.emit('error', err)
    throw err
  }
}

Step-by-Step Breakdown

A fresh ExecutionContext is created for each run, containing:
  • User input
  • Custom data state
  • Empty message/step arrays
  • Session ID for memory scoping
  • Event emitter reference
Source: packages/core/src/context/factory.ts:32-54
Emits the run:start event with the user input and session ID.
Runs all middleware registered for the run:before scope.
If a memory provider is configured, it reads prior conversation history for the session and injects it into ctx.state.messages.Source: packages/core/src/agent/agent.ts:123-127
The configured ReasoningEngine takes over and implements its strategy (ReAct, planning, chain-of-thought, etc.). The engine:
  • Calls the model
  • Executes tools as needed
  • Pushes reasoning steps for observability
  • Returns the final output string
Source: packages/core/src/agent/agent.ts:135
If a memory provider is configured, the full conversation (including tool calls and results) is persisted.Source: packages/core/src/agent/agent.ts:138-143
Runs all middleware registered for the run:after scope.
Emits the run:end event with the final output and execution state.

Agent Configuration

The AgentConfig interface defines all available options:
// Source: packages/core/src/types/index.ts:343-354
export interface AgentConfig<TData = unknown> {
  name: string
  description?: string
  model?: ModelProvider
  tools?: ToolDefinition<TData>[]
  memory?: MemoryProvider
  reasoning?: ReasoningStrategy | ReasoningEngine<TData>
  middleware?: Middleware<TData>[]
  policy?: AgentPolicy
  systemPrompt?: string
  data?: TData
}

Configuration Options

OptionTypeDescription
namestringRequired. Unique identifier for the agent
descriptionstringHuman-readable description
modelModelProviderLLM provider (e.g., OpenAI, Anthropic)
toolsToolDefinition[]Functions the agent can call
memoryMemoryProviderConversation persistence strategy
reasoningReasoningStrategy | ReasoningEngineDecision-making strategy (default: 'react')
middlewareMiddleware[]Lifecycle hooks
policyAgentPolicyResource limits and constraints
systemPromptstringInjected as the first message in every run
dataTDataCustom typed state shared across execution

Fluent API

Agents support a chainable fluent API for configuration:
import { createAgent } from '@agentlib/core'
import { openai } from '@agentlib/providers/openai'
import { BufferMemory } from '@agentlib/memory'

const agent = createAgent({ name: 'assistant' })
  .provider(openai({ model: 'gpt-4' }))
  .memory(new BufferMemory({ maxMessages: 40 }))
  .tool(myCustomTool)
  .use(loggingMiddleware)
  .policy({ maxSteps: 15, timeout: 60000 })
  .reasoning('react')

await agent.run('Hello!')
Source: packages/core/src/agent/agent.ts:55-68

Custom Data State

The data field allows you to define custom typed state that’s accessible in tools and middleware:
interface MyState {
  userId: string
  preferences: Record<string, any>
}

const agent = createAgent<MyState>({
  name: 'personalized-assistant',
  data: {
    userId: 'user-123',
    preferences: {}
  },
  tools: [
    defineTool<MyState>({
      schema: {
        name: 'get_preference',
        description: 'Get a user preference',
        parameters: {
          type: 'object',
          properties: {
            key: { type: 'string' }
          },
          required: ['key']
        }
      },
      execute: async (args, ctx) => {
        // ctx.data is fully typed as MyState
        return ctx.data.preferences[args.key as string]
      }
    })
  ]
})

// Override data for a specific run
await agent.run({
  input: 'What are my preferences?',
  data: { userId: 'user-456', preferences: { theme: 'dark' } }
})

Session Management

Sessions enable multi-turn conversations with memory:
import { BufferMemory } from '@agentlib/memory'

const agent = createAgent({ name: 'assistant' })
  .memory(new BufferMemory())

const sessionId = 'user-abc-session-1'

// First message
await agent.run({ input: 'My name is Alice', sessionId })

// Second message - agent remembers context
await agent.run({ input: 'What is my name?', sessionId })
// => "Your name is Alice."
If you don’t provide a sessionId, a random UUID is generated per run, creating isolated single-turn conversations.

Cancellation

You can cancel a running agent using an AbortSignal:
const controller = new AbortController()

const runPromise = agent.run({
  input: 'Complex task...',
  signal: controller.signal
})

// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000)

try {
  await runPromise
} catch (err) {
  // Handle cancellation
}
Or programmatically from within the agent:
agent.on('tool:before', ({ name }) => {
  if (name === 'dangerous_operation') {
    agent.cancel('Blocked dangerous operation')
  }
})

RunResult

Every agent.run() returns a RunResult containing the output and full execution state:
const result = await agent.run('Calculate 2 + 2')

console.log(result.output) // "The result is 4."
console.log(result.state.steps) // All reasoning steps
console.log(result.state.toolCalls) // All tool invocations
console.log(result.state.usage) // Token usage
// Source: packages/core/src/types/index.ts:241-248
export interface ExecutionState {
  steps: ReasoningStep[]
  messages: ModelMessage[]
  toolCalls: Array<{ call: ToolCall; result: unknown }>
  usage: TokenUsage
  startedAt: Date
  finishedAt?: Date
}

Best Practices

Always configure a model provider before calling run(). If no model is set, execution will throw an error.Source: packages/core/src/agent/agent.ts:111
  1. Use descriptive names - Helps with debugging and observability
  2. Set reasonable policies - Prevent runaway costs with maxSteps, timeout, maxCost
  3. Leverage sessions - Use consistent sessionId values for multi-turn conversations
  4. Type your data - Use TypeScript generics for type-safe custom state
  5. Handle errors - Always wrap agent.run() in try/catch

Next Steps

  • Tools - Learn how to give agents capabilities
  • Memory - Understand conversation persistence
  • Reasoning - Explore different reasoning strategies
  • Middleware - Add lifecycle hooks
  • Events - Observe agent execution

Build docs developers (and LLMs) love