Monitor Your First Agent
Step-by-step guide to instrument your first AI agent with AgenticAnts in under 10 minutes.
Prerequisites
Before you begin, make sure you have:
- AgenticAnts account - Sign up at agenticants.ai
- API keys - Get your public/secret key pair (
pk-ap-... / sk-ap-...) from the dashboard
- Python 3.8+, Node.js 20+ (the JS tracing packages require Node 20+), or Java 17+
- Basic knowledge of AI frameworks (LangChain, OpenAI, etc.)
Step 1: Install AgenticAnts SDK
Python Installation
pip install ants-platform
Node.js Installation
npm install ants-platform
# For the LangChain example below, also install:
npm install @antsplatform/langchain langchain @langchain/openai
The root ants-platform package re-exports the client plus the OpenTelemetry-based
tracing helpers. Framework integrations live in scoped packages
(@antsplatform/langchain, @antsplatform/openai).
Java Installation
Add the dependency to your pom.xml:
<dependency>
<groupId>ai.agenticants</groupId>
<artifactId>ants-platform-java</artifactId>
<version>0.2.0</version>
</dependency>
Gradle: implementation("ai.agenticants:ants-platform-java:0.2.0"). Requires Java 17+.
Step 2: Initialize AgenticAnts
Python Setup
from ants_platform import AntsPlatform
# Initialize AgenticAnts (this also configures the global singleton)
ants = AntsPlatform(
public_key=os.getenv("ANTS_PLATFORM_PUBLIC_KEY"), # "pk-ap-..."
secret_key=os.getenv("ANTS_PLATFORM_SECRET_KEY"), # "sk-ap-..."
host="https://api.agenticants.ai",
)
print("AgenticAnts initialized successfully!")
Node.js Setup
Tracing in the JS SDK is OpenTelemetry-based. Register the AntsPlatformSpanProcessor
once at startup (before the rest of your app runs) — this is what actually ships
spans to AgenticAnts. Put this in an instrumentation.ts you import first.
// instrumentation.ts
const provider = new NodeTracerProvider({
spanProcessors: [
new AntsPlatformSpanProcessor({
publicKey: process.env.ANTS_PLATFORM_PUBLIC_KEY, // "pk-ap-..."
secretKey: process.env.ANTS_PLATFORM_SECRET_KEY, // "sk-ap-..."
baseUrl: 'https://api.agenticants.ai',
}),
],
})
provider.register()
setAntsPlatformTracerProvider(provider) // so observe()/CallbackHandler use this provider
console.log('AgenticAnts tracing initialized successfully!')
Load it before your app code, e.g. node --import ./instrumentation.js app.js, or
import './instrumentation' at the very top of your entry file.
The REST client (new AntsPlatformClient({ publicKey, secretKey, baseUrl })) is a
separate object used for prompts, datasets, and scores — it does not capture
traces. Trace capture is the span processor above.
Java Setup
Tracing in the Java SDK is OpenTelemetry-based. Call AntsPlatformOtel.configure(...)
once at startup (before any instrumented code runs) — this registers the global
OpenTelemetry tracer that all instrumentation uses. The host defaults to
https://api.agenticants.ai, so you only call .baseUrl(...) to override it.
public class App {
public static void main(String[] args) {
AntsPlatformOtel.configure(AntsPlatformOtel.options()
.publicKey(System.getenv("ANTS_PLATFORM_PUBLIC_KEY")) // "pk-ap-..."
.secretKey(System.getenv("ANTS_PLATFORM_SECRET_KEY")) // "sk-ap-..."
.environment("production")); // optional
System.out.println("AgenticAnts tracing initialized successfully!");
}
}
Flush before a short-lived process exits with AntsPlatformOtel.flush() (or
AntsPlatformOtel.shutdown()) so batched spans are delivered.
Step 3: Create Your First Agent
A real agent is more than one LLM call — it plans, calls a tool, and reaches a
dependent service. We'll build a small currency assistant and instrument it
end-to-end so the dashboard shows the full tree: a root agent span, with a
nested generation (LLM call with model + token usage), a tool call, and a
span for the downstream HTTP service. The backend computes cost server-side
from the model + usageDetails/usage_details you report on the generation.
Python Example
start_as_current_observation opens a context-managed observation; anything
opened inside it nests automatically. Note the two Python rules: always set
host explicitly (Step 2), and always pass agent_name= when as_type="agent".
from ants_platform import get_client
client = get_client()
def currency_agent(question: str, amount: float, base: str, quote: str) -> str:
# Root agent span — as_type="agent" REQUIRES agent_name=
with client.start_as_current_observation(
name="currency-agent",
as_type="agent",
agent_name="currency-agent",
):
client.update_current_trace(
name="currency-agent",
input=question,
user_id="demo-user",
session_id="demo-session",
tags=["currency", "tutorial"],
)
# 1. LLM generation — model + usage_details let the backend compute cost
prompt = f"Plan how to answer: {question}"
with client.start_as_current_observation(
name="plan",
as_type="generation",
model="gpt-4o-mini",
input=prompt,
model_parameters={"temperature": 0.2},
) as gen:
plan = f"Convert {amount} {base} to {quote} using the live rate."
gen.update(
output=plan,
usage_details={"input": 48, "output": 12, "total": 60},
)
# 2. Dependent service — a downstream HTTP call as its own span
with client.start_as_current_observation(
name="fetch-rate",
as_type="span",
input={"base": base, "quote": quote},
) as span:
resp = httpx.get(f"https://api.frankfurter.app/latest?from={base}&to={quote}")
rate = resp.json()["rates"][quote]
span.update(output={"rate": rate})
# 3. Tool call — deterministic conversion
with client.start_as_current_observation(
name="currency-convert",
as_type="tool",
input={"amount": amount, "rate": rate},
) as tool:
converted = round(amount * rate, 2)
tool.update(output={quote: converted})
answer = f"{amount} {base} = {converted} {quote} (rate {rate})"
client.update_current_trace(output={"answer": answer})
return answer
# Test your agent
answer = currency_agent("How much is 100 USD in EUR?", 100, "USD", "EUR")
print(f"Agent response: {answer}")
# Flush buffered spans before the process exits (short-lived scripts)
client.flush()
Node.js Example
startActiveObservation runs its callback with the observation as the active
context, so children nest under it. Every generation should carry model and
usageDetails — that's what the backend uses to compute cost.
startActiveObservation,
startObservation,
updateActiveTrace,
getActiveTraceId,
} from 'ants-platform'
async function currencyAgent(
question: string,
amount: number,
base: string,
quote: string,
) {
return startActiveObservation(
'currency-agent',
async () => {
updateActiveTrace({
name: 'currency-agent',
input: question,
userId: 'demo-user',
sessionId: 'demo-session',
tags: ['currency', 'tutorial'],
})
// 1. LLM generation — model + usageDetails drive server-side cost
const prompt = `Plan how to answer: ${question}`
const gen = startObservation(
'plan',
{ model: 'gpt-4o-mini', input: prompt, modelParameters: { temperature: 0.2 } },
{ asType: 'generation' },
)
const plan = `Convert ${amount} ${base} to ${quote} using the live rate.`
gen.update({ output: plan, usageDetails: { input: 48, output: 12, total: 60 } })
gen.end()
// 2. Dependent service — a downstream HTTP call as its own span
const rate = await startActiveObservation(
'fetch-rate',
async (span) => {
span.update({ input: { base, quote } })
const res = await fetch(
`https://api.frankfurter.app/latest?from=${base}&to=${quote}`,
)
const data = await res.json()
span.update({ output: { rate: data.rates[quote] } })
return data.rates[quote] as number
},
{ asType: 'span' },
)
// 3. Tool call — deterministic conversion
const converted = Math.round(amount * rate * 100) / 100
const tool = startObservation(
'currency-convert',
{ input: { amount, rate } },
{ asType: 'tool' },
)
tool.update({ output: { [quote]: converted } })
tool.end()
const answer = `${amount} ${base} = ${converted} ${quote} (rate ${rate})`
updateActiveTrace({ output: answer })
return { answer, traceId: getActiveTraceId() }
},
{ asType: 'agent' },
)
}
// Test your agent
const { answer } = await currencyAgent('How much is 100 USD in EUR?', 100, 'USD', 'EUR')
console.log(`Agent response: ${answer}`)
Prefer to let a framework emit the spans? Wrap the OpenAI client once with
observeOpenAI from @antsplatform/openai (every call is auto-traced as a
generation), or pass a LangChain CallbackHandler per invocation:
import './instrumentation'
import OpenAI from 'openai'
import { observeOpenAI } from '@antsplatform/openai'
import { CallbackHandler } from '@antsplatform/langchain'
const openai = observeOpenAI(new OpenAI()) // every call auto-traced
const handler = new CallbackHandler({ sessionId: 'demo-session', userId: 'demo-user', tags: ['currency'] })
// await chain.invoke(input, { callbacks: [handler] })
Java Example
Ants.observe(name, type, fn) runs its lambda with the observation as the active
context, so anything opened inside it nests automatically. Every generation should
carry model and usage — that's what the backend uses to compute cost. Unlike
Python, Java does not need an agent name: for ObservationType.AGENT the SDK
auto-derives the agent name from the observation name (no agent_name= trap).
public class CurrencyAgent {
public static void main(String[] args) {
AntsPlatformOtel.configure(AntsPlatformOtel.options()
.publicKey(System.getenv("ANTS_PLATFORM_PUBLIC_KEY"))
.secretKey(System.getenv("ANTS_PLATFORM_SECRET_KEY")));
String answer = Ants.observe("currency-agent", ObservationType.AGENT, agent -> {
agent.updateTrace(TraceAttributes.builder()
.name("currency-agent").userId("demo-user").sessionId("demo-session")
.tags(List.of("currency", "tutorial"))
.input("How much is 100 USD in EUR?").build());
// 1. LLM generation — model + usage let the backend compute cost
AntsGeneration gen = Ants.startGeneration("plan")
.model("gpt-4o-mini").modelParameters(Map.of("temperature", 0.2))
.input("Plan how to answer the question");
gen.output("Convert 100 USD to EUR using the live rate.").usage(48, 12).end();
// 2. Dependent service — a downstream call as its own span
double rate = Ants.observe("fetch-rate", ObservationType.SPAN, span -> {
span.input(Map.of("base", "USD", "quote", "EUR"));
double r = 0.92; // call your FX service here
span.output(Map.of("rate", r));
return r;
});
// 3. Tool call — deterministic conversion
double converted = Math.round(100 * rate * 100.0) / 100.0;
AntsObservation tool = Ants.startObservation("currency-convert", ObservationType.TOOL);
tool.input(Map.of("amount", 100, "rate", rate)).output(Map.of("EUR", converted)).end();
String result = "100 USD = " + converted + " EUR";
agent.updateTrace(TraceAttributes.builder().output(result).build());
return result;
});
System.out.println("Agent response: " + answer);
AntsPlatformOtel.flush(); // deliver buffered spans before exit
}
}
Step 4: View Results in Dashboard
-
Open AgenticAnts Dashboard - Go to app.agenticants.ai
-
View Traces - Click on "Traces" to see your agent interactions
-
Analyze Performance - Check metrics like:
- Response time
- Token usage
- Success rate
- Error rate
-
Monitor Costs - View cost breakdown by:
- Model used
- Token consumption
- Time period
Step 5: Add More Monitoring
Track User Interactions
from ants_platform import observe, get_client
@observe(name="customer-support")
def enhanced_customer_support_agent(question: str, user_id: str, session_id: str):
client = get_client()
# Annotate the active trace with user/session context
client.update_current_trace(
name="customer-support",
input=question,
user_id=user_id,
session_id=session_id,
metadata={"agent_type": "customer_support"},
)
# A retriever step — pull relevant docs before answering
with client.start_as_current_observation(
name="retrieve-docs",
as_type="retriever",
input={"query": question},
) as retriever:
docs = ["reset-password.md", "account-recovery.md"]
retriever.update(output={"docs": docs})
# The LLM generation — model + usage_details so the backend computes cost
with client.start_as_current_observation(
name="response_generation",
as_type="generation",
model="gpt-4o-mini",
input={"question": question, "context": docs},
model_parameters={"temperature": 0.7},
) as gen:
response = f"Here's how to reset your password, based on {len(docs)} docs."
gen.update(
output=response,
usage_details={"input": 220, "output": 90, "total": 310},
)
# Record the final result on the trace this function opened
client.update_current_trace(
output=response,
metadata={"success": True},
)
return response
In Java, wrap the unit of work with Ants.observe(...) and open a RETRIEVER
observation before the generation. There is no @observe annotation in Java — use
Ants.observe(name, type, fn) instead.
public String enhancedCustomerSupportAgent(String question, String userId, String sessionId) {
return Ants.observe("customer-support", ObservationType.AGENT, agent -> {
// Annotate the active trace with user/session context
agent.updateTrace(TraceAttributes.builder()
.name("customer-support").input(question)
.userId(userId).sessionId(sessionId)
.metadata(Map.of("agent_type", "customer_support")).build());
// A retriever step — pull relevant docs before answering
List<String> docs = Ants.observe("retrieve-docs", ObservationType.RETRIEVER, retriever -> {
retriever.input(Map.of("query", question));
List<String> found = List.of("reset-password.md", "account-recovery.md");
retriever.output(Map.of("docs", found));
return found;
});
// The LLM generation — model + usage so the backend computes cost
AntsGeneration gen = Ants.startGeneration("response_generation")
.model("gpt-4o-mini").modelParameters(Map.of("temperature", 0.7))
.input(Map.of("question", question, "context", docs));
String response = "Here's how to reset your password, based on " + docs.size() + " docs.";
gen.output(response).usage(220, 90).end();
// Record the final result on the trace this observation opened
agent.updateTrace(TraceAttributes.builder()
.output(response).metadata(Map.of("success", true)).build());
return response;
});
}
Add Error Handling
Wrap any function with observe() to capture it as a span — including its inputs,
output, and errors. The span is recorded on the active trace automatically.
const robustCustomerSupportAgent = observe(
async (question: string, userId: string) => {
// Each step is its own span; on failure mark level: "ERROR" + statusMessage
const validate = startObservation('validate-input', { input: { question } }, { asType: 'span' })
try {
if (!question || question.length < 3) throw new Error('Question too short')
validate.update({ output: { ok: true } })
} catch (e) {
validate.update({ level: 'ERROR', statusMessage: String(e) })
throw e
} finally {
validate.end()
}
const gen = startObservation(
'answer',
{ model: 'gpt-4o-mini', input: question, modelParameters: { temperature: 0.7 } },
{ asType: 'generation' },
)
try {
const response = await currencyAgent(question, 100, 'USD', 'EUR')
gen.update({ output: response.answer, usageDetails: { input: 60, output: 24, total: 84 } })
return response.answer
} catch (error) {
// Record the failure on the span, then return a graceful fallback
gen.update({ level: 'ERROR', statusMessage: String(error) })
return "I'm sorry, I encountered an issue. Please try again or contact support."
} finally {
gen.end()
}
},
{ name: 'customer-support' },
)
In Java, wrap the work with Ants.observe(...) and, on failure, mark the span with
.level("ERROR").statusMessage(...) before ending it (or call obs.recordError(e),
which records the exception and sets the ERROR status for you).
public String robustCustomerSupportAgent(String question) {
return Ants.observe("customer-support", ObservationType.AGENT, agent -> {
// Each step is its own span; on failure mark level "ERROR" + statusMessage
AntsObservation validate = Ants.startObservation("validate-input", ObservationType.SPAN);
validate.input(Map.of("question", question));
try {
if (question == null || question.length() < 3) {
throw new IllegalArgumentException("Question too short");
}
validate.output(Map.of("ok", true));
} catch (Exception e) {
validate.level("ERROR").statusMessage(e.getMessage());
validate.end();
throw e;
}
validate.end();
AntsGeneration gen = Ants.startGeneration("answer")
.model("gpt-4o-mini").modelParameters(Map.of("temperature", 0.7))
.input(question);
try {
String response = "100 USD = 92.0 EUR"; // call your model here
gen.output(response).usage(60, 24).end();
return response;
} catch (Exception e) {
// Record the failure on the span, then return a graceful fallback
gen.level("ERROR").statusMessage(e.getMessage());
gen.end();
return "I'm sorry, I encountered an issue. Please try again or contact support.";
}
});
}
Step 6: Set Up Alerts
Alerts are a dashboard feature — there is no programmatic alerts API in the SDK. In
app.agenticants.ai, open Alerts and create rules
such as High Error Rate (error rate > 5% over 5m) or Slow Response Time (p95
latency > 3000ms over 10m), and pick email/Slack channels. Once your agent emits
traces (above), the latency and error data these alerts watch is populated
automatically.
Create Cost Alerts
Alerts are configured in the dashboard, not via the SDK. In
app.agenticants.ai, open Alerts and create rules
such as High Daily Cost or Monthly Budget with email/Slack channels. Once your
agent is emitting traces (above), the cost and latency data these alerts watch is
populated automatically.
Step 7: Monitor and Optimize
View Key Metrics
There is no SDK metrics-query API. Total requests, average response time, error rate,
total cost, and success rate for the customer-support agent are available in the
Dashboard and Traces views at app.agenticants.ai,
filterable by agent, model, and time range (e.g. last 7 days). These views are driven
directly by the traces your agent emits.
Analyze Trends
Request volume, latency percentiles, token usage, and cost trends are available in
the Dashboard and Traces views at app.agenticants.ai,
filterable by agent, model, and time range. There is no SDK metrics-query API in the
JS SDK — the dashboards are driven directly by the traces your agent emits.
Common Issues and Solutions
Issue: "API Key not found"
Solution: Make sure both keys (and the base URL) are set in environment variables:
export ANTS_PLATFORM_PUBLIC_KEY="pk-ap-..."
export ANTS_PLATFORM_SECRET_KEY="sk-ap-..."
export ANTS_PLATFORM_BASE_URL="https://api.agenticants.ai"
Issue: "No traces appearing in dashboard"
Solution (JS): Confirm your instrumentation file is imported before any
instrumented code runs, that every span is ended (observe() does this for you), and
that spans are flushed before the process exits. For short-lived scripts, call
await provider.forceFlush() (or provider.shutdown()) before exiting so batched
spans are sent.
Solution (Java): Ensure AntsPlatformOtel.configure(...) runs before any
instrumented code, and call AntsPlatformOtel.flush() (or
AntsPlatformOtel.shutdown()) before a short-lived process exits so batched spans
are delivered.
Issue: "High latency"
Solution:
- Check your network connection
- Consider using a different model
- Implement caching for repeated queries
Issue: "High costs"
Solution:
- Use smaller models for simple tasks
- Implement response caching
- Optimize prompts to reduce token usage
Next Steps
Now that you have your first agent monitored, here's what to explore next:
- Advanced Monitoring - Learn about multi-agent systems
- Cost Optimization - Reduce costs with our optimization guide
- Production Deployment - Deploy to production with best practices
- Integration Guides - Connect with LangChain or other frameworks
Example Projects
Check out these complete examples:
Support
Need help? We're here for you:
Congratulations! 🎉 You've successfully instrumented your first AI agent with AgenticAnts. You now have complete visibility into your agent's performance, costs, and behavior.