Real Agent Integration

How AOP works with real autonomous agents — where the LLM generates the reasoning, not you.

The key concept

In the quickstart, you saw hardcoded strings like aop.thought('Starting with broad search'). That simplified example shows the shape of AOP events, but in a real agent, the LLM generates that content.

Your agent calls an LLM, the LLM reasons about what to do, picks tools, evaluates results. AOP captures that reasoning as it happens. You write the wiring once — after that, the agent narrates its own behavior.

The content of aop.thought(), aop.decision(), and aop.uncertainty()should come from the LLM's output — not from strings you write yourself. That's what makes AOP agent-native: the agent describes its own thinking.

Anthropic SDK (Claude)

Wrap a Claude tool-use loop with AOP to capture every thought and action:

import Anthropic from '@anthropic-ai/sdk'
import { AOPClient } from '@useaop/sdk'

const anthropic = new Anthropic()
const aop = new AOPClient({ agentId: 'research-agent' })

await aop.sessionStarted({ goal: userQuery, llm: 'claude-sonnet-4-6' })

const messages: Anthropic.MessageParam[] = [{ role: 'user', content: userQuery }]

while (true) {
  const response = await anthropic.messages.create({
    model: 'claude-sonnet-4-6',
    max_tokens: 4096,
    tools: myTools,
    messages,
  })

  // Capture the LLM's reasoning as a thought event
  const textBlock = response.content.find(b => b.type === 'text')
  if (textBlock && textBlock.type === 'text') {
    await aop.thought(textBlock.text, { confidence: 'high' })
  }

  // If the LLM chose to use a tool, capture it
  if (response.stop_reason === 'tool_use') {
    for (const block of response.content) {
      if (block.type === 'tool_use') {
        // Record the tool call
        const callId = await aop.toolStart(block.name, block.input as Record<string, unknown>)

        // Execute the actual tool
        const start = Date.now()
        const result = await executeMyTool(block.name, block.input)
        const latency = Date.now() - start

        // Record the result
        await aop.toolEnd(block.name, result.success, result.summary, latency, {
          toolCallId: callId,
          tokenSpendDelta: response.usage.output_tokens,
        })

        // Feed result back to Claude
        messages.push({ role: 'assistant', content: response.content })
        messages.push({
          role: 'user',
          content: [{ type: 'tool_result', tool_use_id: block.id, content: result.output }],
        })
      }
    }
  } else {
    // LLM is done — end the session
    await aop.sessionEnded('completed', {
      outcome_summary: textBlock?.type === 'text' ? textBlock.text.slice(0, 200) : 'Done',
      total_cost_usd: estimateCost(response.usage),
    })
    break
  }
}

Notice that aop.thought(textBlock.text)passes the LLM's actual response — not a string you wrote. The agent is describing its own reasoning.

LangChain

Use a LangChain callback handler to automatically emit AOP events for every step in a chain or agent:

import { AOPClient } from '@useaop/sdk'
import { BaseCallbackHandler } from 'langchain/callbacks'

class AOPCallbackHandler extends BaseCallbackHandler {
  name = 'AOPCallbackHandler'
  private aop: AOPClient
  private toolTimers = new Map<string, number>()

  constructor(aop: AOPClient) {
    super()
    this.aop = aop
  }

  async handleLLMEnd(output: any) {
    // The LLM's response becomes a thought event
    const text = output.generations?.[0]?.[0]?.text
    if (text) {
      await this.aop.thought(text, { confidence: 'high' })
    }
  }

  async handleToolStart(tool: any, input: string, runId: string) {
    this.toolTimers.set(runId, Date.now())
    await this.aop.toolStart(tool.name, { input }, { toolCallId: runId })
  }

  async handleToolEnd(output: string, runId: string) {
    const latency = Date.now() - (this.toolTimers.get(runId) ?? Date.now())
    await this.aop.toolEnd('tool', true, output.slice(0, 200), latency, { toolCallId: runId })
    this.toolTimers.delete(runId)
  }

  async handleToolError(error: any, runId: string) {
    const latency = Date.now() - (this.toolTimers.get(runId) ?? Date.now())
    await this.aop.toolEnd('tool', false, '', latency, { toolCallId: runId, error: String(error) })
    this.toolTimers.delete(runId)
  }
}

// Usage — add the handler to any chain or agent
const aop = new AOPClient({ agentId: 'langchain-agent' })
await aop.sessionStarted({ goal: 'Research task', framework: 'langchain' })

const agent = createReActAgent({ llm, tools })
const result = await agent.invoke(
  { input: 'Research AI observability tools' },
  { callbacks: [new AOPCallbackHandler(aop)] }
)

await aop.sessionEnded('completed', { outcome_summary: result.output })

With this pattern, every LLM call and tool invocation in your LangChain agent automatically emits AOP events. No manual instrumentation needed beyond the callback handler.

CrewAI

Wrap CrewAI task execution with AOP events:

import { AOPClient } from '@useaop/sdk'
import { Crew, Agent, Task } from 'crewai'

const aop = new AOPClient({ agentId: 'crewai-orchestrator' })
await aop.sessionStarted({ goal: 'Multi-agent research', framework: 'crewai' })

// Spawn a child AOP session for each CrewAI agent
const researcher = new Agent({ role: 'Researcher', goal: 'Find data', llm: 'claude-sonnet-4-6' })
const childAop = await aop.agentSpawn('crewai-researcher', 'Find market data', {
  reason: 'Delegating research to specialized agent'
})

// Wrap task execution
const task = new Task({ description: 'Research market trends', agent: researcher })

// Before execution
await childAop.sessionStarted({ goal: task.description, framework: 'crewai' })

// Execute the task
const result = await crew.kickoff()

// After execution — the result contains the agent's reasoning
await childAop.thought(result.output, { confidence: 'high' })
await childAop.sessionEnded('completed', { outcome_summary: result.output.slice(0, 200) })
await aop.sessionEnded('completed', { outcome_summary: 'Crew completed all tasks' })

Custom agent loop

For agents built from scratch without a framework, the pattern is the same — wrap your LLM calls and tool executions:

import { AOPClient } from '@useaop/sdk'

const aop = new AOPClient({ agentId: 'custom-agent' })
await aop.sessionStarted({ goal: task })

while (!done) {
  // Your LLM call
  const llmResponse = await callLLM(prompt)

  // The LLM's reasoning becomes the thought
  await aop.thought(llmResponse.reasoning)

  // If the LLM decided to use a tool
  if (llmResponse.toolCall) {
    const callId = await aop.toolStart(llmResponse.toolCall.name, llmResponse.toolCall.args)
    const result = await runTool(llmResponse.toolCall)
    await aop.toolEnd(llmResponse.toolCall.name, result.ok, result.summary, result.ms, {
      toolCallId: callId,
    })
  }

  // If the LLM expressed uncertainty
  if (llmResponse.confidence === 'low') {
    await aop.uncertainty(llmResponse.concern, 'low', llmResponse.plan)
  }

  // If the LLM changed its approach
  if (llmResponse.newGoal) {
    await aop.goal(llmResponse.newGoal, previousGoal, {
      reasonForChange: llmResponse.reason,
    })
  }
}

await aop.sessionEnded('completed', { outcome_summary: finalResult })

Key takeaway

You write the wiring once. After that, the LLM generates the content of every thought(), picks every toolStart(), and produces every result. AOP just captures what the agent is already doing — it doesn't change the agent's behavior.

Next steps