Debugging AI Agents
Debug and troubleshoot agent issues effectively with comprehensive tracing instrumentation and the Agentic Ants dashboard.
Overview
Debugging AI agents requires good observability. The Agentic Ants SDK captures detailed traces of your agent's execution; you then inspect those traces, errors, and performance metrics in the dashboard at https://app.agenticants.ai. This guide covers:
- Common Issues - Typical problems and how instrumentation surfaces them
- Debugging Tools - Tracing primitives for capturing what your agent does
- Trace Analysis - Adding rich context to traces so they're easy to read later
- Performance Debugging - Capturing timing on individual operations
- Error Analysis - Recording failures on spans so they show up in the dashboard
The SDK does not expose a programmatic API for querying traces, metrics, trends, or alerts. Those are dashboard features at https://app.agenticants.ai. What you do in code is instrument your agent so that rich data flows to the dashboard.
Setup
Install the SDK and (optionally) the integrations you use. Tracing requires Node.js 20+.
# JavaScript / TypeScript
npm install ants-platform
npm install @antsplatform/openai @antsplatform/langchain # optional integrations
# Python
pip install ants-platform
Set credentials as environment variables. Credentials are a public/secret key pair (pk-ap-... / sk-ap-...).
export ANTS_PLATFORM_PUBLIC_KEY="pk-ap-..."
export ANTS_PLATFORM_SECRET_KEY="sk-ap-..."
In TypeScript, configure tracing once at startup in an instrumentation.ts that is imported before any instrumented code runs:
// instrumentation.ts — import this first, before anything you want traced
const provider = new NodeTracerProvider({
spanProcessors: [
new AntsPlatformSpanProcessor({
publicKey: process.env.ANTS_PLATFORM_PUBLIC_KEY,
secretKey: process.env.ANTS_PLATFORM_SECRET_KEY,
baseUrl: "https://api.agenticants.ai",
}),
],
})
provider.register()
setAntsPlatformTracerProvider(provider)
// In short-lived scripts, flush before the process exits:
// await provider.forceFlush()
In Python, configure the client explicitly. Always set host — the default host is not yet correct — and reuse the singleton via get_client():
from ants_platform import AntsPlatform, get_client
AntsPlatform(
public_key=os.environ["ANTS_PLATFORM_PUBLIC_KEY"],
secret_key=os.environ["ANTS_PLATFORM_SECRET_KEY"],
host="https://api.agenticants.ai", # always set host explicitly
)
client = get_client()
# Call client.flush() before the process exits in short-lived scripts.
In Java (17+), add the Maven dependency and configure the global tracer once at startup:
<dependency>
<groupId>ai.agenticants</groupId>
<artifactId>ants-platform-java</artifactId>
<version>0.2.0</version>
</dependency>
// Call once at startup — registers the global OpenTelemetry tracer.
// host defaults to https://api.agenticants.ai; only call .baseUrl(...) to override.
AntsPlatformOtel.configure(AntsPlatformOtel.options()
.publicKey(System.getenv("ANTS_PLATFORM_PUBLIC_KEY"))
.secretKey(System.getenv("ANTS_PLATFORM_SECRET_KEY"))
.environment("production"));
// In short-lived processes, flush before exit so buffered spans are delivered:
// AntsPlatformOtel.flush(); // or AntsPlatformOtel.shutdown();
Common Issues
1. Agent Not Responding
When an agent hangs or never returns, the fastest path to a diagnosis is a trace that shows exactly where execution stalled. Wrap each step in an observation so the dashboard timeline shows which operation is slow or never completes.
from ants_platform import observe, get_client
@observe(name="handle_request")
def handle_request(agent_name: str, user_input: str):
client = get_client()
# Wrap the whole request in an agent observation. as_type="agent" REQUIRES agent_name.
with client.start_as_current_observation(
name=f"agent:{agent_name}", as_type="agent", agent_name=agent_name
):
client.update_current_trace(
name=f"agent:{agent_name}",
input={"agent": agent_name, "user_input": user_input},
metadata={"agent": agent_name, "phase": "diagnosis"},
)
# Time each step as its own span so a stall is obvious in the dashboard.
with client.start_as_current_observation(
name="retrieve_context", as_type="retriever", input={"query": user_input}
) as span:
start = time.time()
context = retrieve_context(user_input)
span.update(
output={"num_docs": len(context)},
metadata={"duration_ms": int((time.time() - start) * 1000)},
)
with client.start_as_current_observation(
name="generate_response", as_type="generation", model="gpt-4o-mini",
input={"context": context, "user_input": user_input},
) as gen:
start = time.time()
response = generate_response(context, user_input)
gen.update(
output=response,
usage_details={"input": 312, "output": 96, "total": 408},
metadata={"duration_ms": int((time.time() - start) * 1000)},
)
client.update_current_trace(output={"response": response})
return response
# Flush before a short-lived process exits so the trace is delivered.
get_client().flush()
In Java, wrap the request with Ants.observe (there is no @observe annotation). For ObservationType.AGENT the SDK auto-derives the agent name from the observation name, so there is no separate agent_name argument. Each child observation nests automatically under the active span:
public class HandleRequest {
public static String handleRequest(String agentName, String userInput) {
return Ants.observe("agent:" + agentName, ObservationType.AGENT, agent -> {
agent.updateTrace(TraceAttributes.builder()
.name("agent:" + agentName)
.input(Map.of("agent", agentName, "user_input", userInput))
.metadata(Map.of("agent", agentName, "phase", "diagnosis"))
.build());
// Time each step as its own span so a stall is obvious in the dashboard.
List<String> context = Ants.observe("retrieve_context", ObservationType.RETRIEVER, span -> {
span.input(Map.of("query", userInput));
long start = System.currentTimeMillis();
List<String> docs = retrieveContext(userInput);
span.output(Map.of("num_docs", docs.size()))
.metadata(Map.of("duration_ms", System.currentTimeMillis() - start));
return docs;
});
// Report the model call as a generation so token/cost data is captured.
AntsGeneration gen = Ants.startGeneration("generate_response")
.model("gpt-4o-mini")
.input(Map.of("context", context, "user_input", userInput));
long start = System.currentTimeMillis();
String response = generateResponse(context, userInput);
gen.output(response)
.usage(312, 96)
.metadata(Map.of("duration_ms", System.currentTimeMillis() - start))
.end();
agent.updateTrace(TraceAttributes.builder().output(Map.of("response", response)).build());
return response;
});
}
public static void main(String[] args) {
handleRequest("support", "Why is my order late?");
// Flush before a short-lived process exits so the trace is delivered.
AntsPlatformOtel.flush();
}
}
Open the trace in the dashboard at https://app.agenticants.ai. The span that has no end time (or a very long duration) is where the agent is stalling. Filtering by status and time range, and viewing aggregate response-time metrics, are dashboard features — there is no SDK call for them.
2. High Error Rates
To debug elevated error rates, make sure failures are recorded on the relevant span so they surface in the dashboard with their input and stack context. Use startObservation and mark the span when an error occurs.
startActiveObservation,
startObservation,
updateActiveTrace,
getActiveTraceId,
} from "ants-platform"
async function handleRequest(agentName: string, userInput: string) {
return startActiveObservation(
`agent:${agentName}`,
async () => {
updateActiveTrace({
name: `agent:${agentName}`,
input: { agent: agentName, userInput },
metadata: { agent: agentName, phase: "diagnosis" },
})
// The model call is a child generation so token/cost data is captured.
const gen = startObservation(
"generate",
{
model: "gpt-4o-mini",
input: userInput,
modelParameters: { temperature: 0.2 },
},
{ asType: "generation" }
)
try {
const response = await runAgent(agentName, userInput)
gen.update({
output: response,
usageDetails: { input: 256, output: 84, total: 340 },
})
gen.end()
updateActiveTrace({ output: { response } })
console.log("trace:", getActiveTraceId())
return response
} catch (err) {
// Record the failure on the child span so it is searchable in the dashboard.
gen.update({
level: "ERROR",
statusMessage: err instanceof Error ? err.message : String(err),
metadata: {
errorType: err instanceof Error ? err.name : "unknown",
input: userInput.substring(0, 100),
},
})
gen.end()
throw err
}
},
{ asType: "agent" }
)
}
In Java, start the child generation manually, mark it on failure inside the catch, and always .end() it in a finally. Setting .level("ERROR") plus .statusMessage(...) surfaces the failure (with its message) in the dashboard:
public static String handleRequest(String agentName, String userInput) {
return Ants.observe("agent:" + agentName, ObservationType.AGENT, agent -> {
agent.updateTrace(TraceAttributes.builder()
.name("agent:" + agentName)
.input(Map.of("agent", agentName, "userInput", userInput))
.metadata(Map.of("agent", agentName, "phase", "diagnosis"))
.build());
// The model call is a child generation so token/cost data is captured.
AntsGeneration gen = Ants.startGeneration("generate")
.model("gpt-4o-mini")
.modelParameters(Map.of("temperature", 0.2))
.input(userInput);
try {
String response = runAgent(agentName, userInput);
gen.output(response).usage(256, 84);
agent.updateTrace(TraceAttributes.builder().output(Map.of("response", response)).build());
return response;
} catch (RuntimeException e) {
// Record the failure on the child span so it is searchable in the dashboard.
gen.level("ERROR").statusMessage(e.getMessage())
.metadata(Map.of("errorType", e.getClass().getSimpleName()));
// (or: gen.recordError(e); which records the exception + ERROR status)
throw e;
} finally {
gen.end();
}
});
}
Once errors are recorded this way, use the dashboard at https://app.agenticants.ai to review error rate, group failures by type, and inspect recent failing traces. The SDK does not expose a metrics or trace-query API; aggregation and pattern analysis are dashboard features.
To debug latency, instrument each sub-operation as its own observation. The dashboard then shows per-step timing, so you can see which operation dominates the total duration.
from ants_platform import observe, get_client
@observe(name="run_agent")
def run_agent(agent_name: str, user_input: str):
client = get_client()
with client.start_as_current_observation(
name=f"agent:{agent_name}", as_type="agent", agent_name=agent_name
):
client.update_current_trace(
metadata={"agent": agent_name}, input={"user_input": user_input}
)
steps = ["retrieve_context", "build_prompt", "call_model", "post_process"]
for step_name in steps:
with client.start_as_current_observation(
name=step_name, as_type="span", input={"step": step_name}
) as span:
start = time.time()
run_step(step_name, user_input)
elapsed_ms = int((time.time() - start) * 1000)
span.update(metadata={"duration_ms": elapsed_ms})
return "done"
get_client().flush()
In Java, use Ants.observe to wrap each unit of work. The span's duration is captured automatically; you can optionally record explicit timing in .metadata(...):
public static String runAgent(String agentName, String userInput) {
return Ants.observe("agent:" + agentName, ObservationType.AGENT, agent -> {
agent.updateTrace(TraceAttributes.builder()
.metadata(Map.of("agent", agentName))
.input(Map.of("user_input", userInput))
.build());
List<String> steps = List.of("retrieve_context", "build_prompt", "call_model", "post_process");
for (String stepName : steps) {
// Each step is its own span — duration is captured automatically.
Ants.startActiveObservation(stepName, ObservationType.SPAN, span -> {
span.input(Map.of("step", stepName));
long start = System.currentTimeMillis();
runStep(stepName, userInput);
span.metadata(Map.of("duration_ms", System.currentTimeMillis() - start));
});
}
return "done";
});
}
Call AntsPlatformOtel.flush() before a short-lived process exits. With per-step spans in place, open the trace in https://app.agenticants.ai to see which operation is the bottleneck. Aggregate latency (average, P95, P99), throughput, and trend charts are dashboard views — there is no programmatic metrics API in the SDK.
1. Trace Analysis
Rich, well-named spans make traces easy to analyze in the dashboard. Name observations clearly and attach the metadata you'll want to filter on later.
from ants_platform import observe, get_client
@observe(name="answer_question")
def answer_question(question: str, user_id: str):
client = get_client()
with client.start_as_current_observation(
name="answer_question", as_type="agent", agent_name="qa-agent"
):
# Attach trace-level context for easy filtering in the dashboard.
client.update_current_trace(
name="answer_question",
user_id=user_id,
session_id="qa-session-42",
input={"question": question},
metadata={"feature": "qa"},
tags=["qa", "production"],
)
with client.start_as_current_observation(
name="retrieve", as_type="retriever", input={"query": question}
) as span:
docs = retrieve(question)
span.update(output={"docs": docs}, metadata={"num_docs": len(docs)})
# Use a generation observation for LLM calls so token/cost data is captured.
with client.start_as_current_observation(
name="llm_answer", as_type="generation", model="gpt-4o",
input={"question": question, "docs": docs},
) as gen:
answer = call_model(question, docs)
gen.update(
output=answer,
usage_details={"input": 540, "output": 120, "total": 660},
)
client.update_current_trace(output={"answer": answer})
# Capture the id so you can fetch this exact trace later via the REST API.
trace_id = client.get_current_trace_id()
return answer, trace_id
get_client().flush()
Open the resulting trace in https://app.agenticants.ai. The span tree, per-span durations, statuses, and metadata you attached are all visible there. Identifying long durations or failed spans is done visually in the dashboard rather than through an SDK query API.
To fetch a specific trace programmatically (for example in a regression check), grab its id from the active context and call GET /api/public/traces/{id} with HTTP Basic auth using your public/secret key pair. There is no SDK trace-query API beyond this single-trace endpoint.
# trace_id = client.get_current_trace_id() # captured during the run
resp = requests.get(
f"https://api.agenticants.ai/api/public/traces/{trace_id}",
auth=(os.environ["ANTS_PLATFORM_PUBLIC_KEY"], os.environ["ANTS_PLATFORM_SECRET_KEY"]),
)
trace = resp.json()
# Inspect observations, their level/statusMessage, and computed cost/usage.
print(trace["id"], [o["level"] for o in trace["observations"]])
2. Error Analysis
Recording failures on spans (as shown in "High Error Rates" above) is what makes error analysis possible. In TypeScript you can also wrap a whole function with observe and mark the active observation on failure.
// observe() wraps the function in an observation that becomes the active context,
// so updateActiveObservation / updateActiveTrace target it. Capturing the exception
// here marks the wrapping observation ERROR before the error re-propagates.
const analyzedHandler = observe(
async (agentName: string, userInput: string) => {
updateActiveTrace({
name: `agent:${agentName}`,
input: { agent: agentName, userInput },
metadata: { agent: agentName },
})
try {
const response = await runAgent(agentName, userInput)
updateActiveTrace({ output: { response } })
return response
} catch (err) {
updateActiveObservation({
level: "ERROR",
statusMessage: err instanceof Error ? err.message : String(err),
metadata: {
errorType: err instanceof Error ? err.name : "unknown",
input: userInput.substring(0, 100),
},
})
throw err
}
},
{ asType: "agent" }
)
After failures are captured this way, the dashboard at https://app.agenticants.ai provides the error summary, error-rate breakdown by type, and the list of recent failing traces. None of those aggregations exist as SDK calls.
For performance analysis, capture precise timing on the operations you care about. Use a generation span for model calls and a regular span for everything else.
from ants_platform import observe, get_client
@observe(name="serve_request")
def serve_request(agent_name: str, user_input: str):
client = get_client()
with client.start_as_current_observation(
name=f"agent:{agent_name}", as_type="agent", agent_name=agent_name
):
client.update_current_trace(
metadata={"agent": agent_name}, input={"user_input": user_input}
)
with client.start_as_current_observation(
name="model_call", as_type="generation", model="gpt-4o",
input={"user_input": user_input},
) as gen:
start = time.time()
output = call_model(user_input)
gen.update(
output=output,
usage_details={"input": 220, "output": 80, "total": 300},
metadata={"latency_ms": int((time.time() - start) * 1000)},
)
client.update_current_trace(output={"text": output})
return output
get_client().flush()
Per-request timing is then visible on each trace in https://app.agenticants.ai. Request rate, average/P95/P99 latency, error and success rates, and performance trends over time are all dashboard charts; the SDK has no API to query them programmatically.
Debugging Workflows
1. Systematic Debugging
A systematic approach to debugging an agent in code is to ensure every stage is instrumented, so a single trace tells the whole story. Combine the @observe decorator with nested spans for each stage.
from ants_platform import observe, get_client
@observe(name="systematic_run")
def systematic_run(agent_name: str, user_input: str):
client = get_client()
with client.start_as_current_observation(
name=f"agent:{agent_name}", as_type="agent", agent_name=agent_name
):
client.update_current_trace(
name=f"agent:{agent_name}",
input={"agent": agent_name, "user_input": user_input},
metadata={"agent": agent_name},
)
with client.start_as_current_observation(
name="validate_input", as_type="span", input={"user_input": user_input}
):
validate_input(user_input)
with client.start_as_current_observation(
name="retrieve_context", as_type="retriever", input={"query": user_input}
) as span:
context = retrieve_context(user_input)
span.update(output={"docs": context}, metadata={"num_docs": len(context)})
with client.start_as_current_observation(
name="generate", as_type="generation", model="gpt-4o",
input={"context": context, "user_input": user_input},
) as gen:
response = generate(context, user_input)
gen.update(
output=response,
usage_details={"input": 480, "output": 110, "total": 590},
)
client.update_current_trace(output={"response": response})
return response
get_client().flush()
Once the run is fully instrumented, walk through the trace top to bottom in https://app.agenticants.ai. Checking status, recent errors, performance, and resource usage is done from the dashboard views rather than via SDK calls.
2. Automated Tracing of Integrations
Rather than hand-instrumenting every call, use the integration wrappers so your LLM and framework calls are traced automatically. This is the most reliable way to keep debugging data flowing.
// Every call made through this client is automatically traced.
const openai = observeOpenAI(new OpenAI())
async function answer(userInput: string) {
const completion = await openai.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: userInput }],
})
return completion.choices[0].message.content
}
For LangChain, attach the callback handler per invocation:
const handler = new CallbackHandler({
sessionId: "debug-session",
tags: ["debugging"],
})
const prompt = ChatPromptTemplate.fromMessages([["human", "{question}"]])
const model = new ChatOpenAI({ model: "gpt-4o" })
const chain = prompt.pipe(model)
const result = await chain.invoke(
{ question: "Why is my agent slow?" },
{ callbacks: [handler] }
)
The auto-captured traces appear in https://app.agenticants.ai, where you review them, filter by error status, and watch latency trends. There is no SDK API for configuring alert rules or querying these results — alerting and metrics are dashboard features.
Best Practices
1. Debugging Checklist
A practical debugging checklist is about what you instrument, not about querying the SDK. Use the dashboard to inspect the results.
- Instrument the entry point with
@observe (Python) or observe (TypeScript) so every request produces a trace.
- Wrap each stage in an observation (
start_as_current_observation(as_type="span") / startObservation(name, body, { asType: "span" })) so timing and failures are attributable to a specific step.
- Use generation observations (
as_type="generation" / { asType: "generation" }) for LLM calls, carrying model and usageDetails so tokens and server-computed cost are captured.
- Record errors on the span with
level="ERROR" and a statusMessage so failures are searchable.
- Attach trace-level context (
update_current_trace / updateActiveTrace) with user id, session, and feature metadata for filtering.
- Flush before exit (
client.flush() / provider.forceFlush()) in short-lived scripts.
Then open https://app.agenticants.ai to review status, recent errors, performance metrics, and resource usage. Those views are provided by the dashboard, not by SDK queries.
The Python instrumentation for the checklist above looks like this:
from ants_platform import observe, get_client
@observe(name="checklist_run", capture_input=True, capture_output=True)
def checklist_run(agent_name: str, user_input: str):
client = get_client()
with client.start_as_current_observation(
name=f"agent:{agent_name}", as_type="agent", agent_name=agent_name
):
client.update_current_trace(
user_id="user-123",
session_id="support-session-9",
metadata={"agent": agent_name, "feature": "support"},
tags=["support"],
)
with client.start_as_current_observation(
name="step_validate", as_type="span", input={"user_input": user_input}
):
validate_input(user_input)
with client.start_as_current_observation(
name="step_generate", as_type="generation", model="gpt-4o",
input={"user_input": user_input},
) as gen:
try:
response = generate(user_input)
gen.update(
output=response,
usage_details={"input": 180, "output": 60, "total": 240},
)
except Exception as exc:
# Capture the exception on the span: level + status_message.
gen.update(level="ERROR", status_message=str(exc))
raise
client.update_current_trace(output={"response": response})
return response
get_client().flush()
2. Debugging Strategy
A repeatable debugging strategy in code is: instrument broadly, name spans consistently, and let the dashboard do the analysis. In TypeScript, a single instrumented entry point with consistent span names gives you a strategy you can apply to any agent.
const runWithStrategy = observe(
async (agentName: string, userInput: string) => {
updateActiveTrace({
name: `agent:${agentName}`,
input: { agent: agentName, userInput },
metadata: { agent: agentName },
})
// Phase 1: assess — instrument the entry point (done by `observe`).
// Phase 2: identify — give every operation a stable, searchable name.
const assess = startObservation(
"phase:assess",
{ input: { userInput } },
{ asType: "retriever" }
)
const context = await retrieveContext(userInput)
assess.update({ output: { context }, metadata: { numDocs: context.length } })
assess.end()
// Phase 3: model call as a generation — carry model + usageDetails.
const gen = startObservation(
"phase:generate",
{ model: "gpt-4o-mini", input: { context, userInput } },
{ asType: "generation" }
)
const response = await generate(context, userInput)
gen.update({
output: response,
usageDetails: { input: 300, output: 90, total: 390 },
})
gen.end()
updateActiveTrace({ output: { response } })
return response
},
{ asType: "agent" }
)
With consistent instrumentation in place, root-cause analysis, solution verification, and trend comparison happen in https://app.agenticants.ai. The SDK does not provide programmatic assessment, root-cause, or verification APIs — those workflows live in the dashboard.
Troubleshooting
Common Debugging Issues
Issue: Agent not responding
Instrument the request so the stalling step is visible in the trace timeline, then inspect it in the dashboard.
from ants_platform import observe, get_client
@observe(name="debug_not_responding")
def debug_not_responding(agent_name: str, user_input: str):
client = get_client()
with client.start_as_current_observation(
name=f"agent:{agent_name}", as_type="agent", agent_name=agent_name
):
client.update_current_trace(
metadata={"agent": agent_name}, input={"user_input": user_input}
)
with client.start_as_current_observation(name="step_a", as_type="span"):
step_a(user_input)
with client.start_as_current_observation(name="step_b", as_type="tool"):
step_b(user_input)
# In a short-lived script, flush so the trace is delivered before exit.
get_client().flush()
Open the trace in https://app.agenticants.ai; the span with no end time is where the agent stalls.
Issue: High error rates
Record failures on the span so they are searchable, then review the error breakdown in the dashboard.
async function debugHighErrorRates(agentName: string, userInput: string) {
return startActiveObservation(
`agent:${agentName}`,
async () => {
const gen = startObservation(
"run",
{ model: "gpt-4o-mini", input: userInput },
{ asType: "generation" }
)
try {
const response = await runAgent(agentName, userInput)
gen.update({
output: response,
usageDetails: { input: 200, output: 70, total: 270 },
})
gen.end()
return response
} catch (err) {
gen.update({
level: "ERROR",
statusMessage: err instanceof Error ? err.message : String(err),
metadata: { errorType: err instanceof Error ? err.name : "unknown" },
})
gen.end()
throw err
}
},
{ asType: "agent" }
)
}
Then use https://app.agenticants.ai to see total errors, group them by type, and inspect recent failing traces.
Next Steps
Example Projects
You now have the instrumentation patterns needed to capture rich traces, and you know where to analyze them: the dashboard at https://app.agenticants.ai.