Docs/Guides/Debugging

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+.

shell
# 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-...).

shell
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:

typescript
// 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():

python
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:

xml
<dependency> <groupId>ai.agenticants</groupId> <artifactId>ants-platform-java</artifactId> <version>0.2.0</version> </dependency>
java
// 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.

python
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:

java
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.

typescript
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:

java
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.

3. Performance Issues

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.

python
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(...):

java
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.

Debugging Tools

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.

python
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.

python
# 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.

typescript
// 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.

3. Performance Analysis

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.

python
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.

python
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.

typescript
// 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:

typescript
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.

  1. Instrument the entry point with @observe (Python) or observe (TypeScript) so every request produces a trace.
  2. 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.
  3. Use generation observations (as_type="generation" / { asType: "generation" }) for LLM calls, carrying model and usageDetails so tokens and server-computed cost are captured.
  4. Record errors on the span with level="ERROR" and a statusMessage so failures are searchable.
  5. Attach trace-level context (update_current_trace / updateActiveTrace) with user id, session, and feature metadata for filtering.
  6. 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:

python
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.

typescript
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.

python
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.

typescript
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.

© 2026 ANTS Platform, Inc.Docs v1.0 · Last updated June 2026