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.
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
- Event types — when to use each event type
- Multi-agent — parent-child session tracking
- SDK reference — full API documentation