Compare commits

11 Commits
v0.1.0 ... main

Author SHA1 Message Date
Vectry
5b388484f8 feat: syntax highlighting with shiki and copy-to-clipboard for all docs code blocks 2026-02-10 04:01:59 +00:00
Vectry
42b5379ce1 feat: coding agent tab in trace detail view
- Add conditional 'Agent' tab for traces tagged 'opencode'
- Tool usage breakdown: horizontal bar chart by tool category (file/search/shell/LSP)
- File changes timeline: chronological view of file reads, edits, and creates
- Pure CSS charts, no external dependencies
2026-02-10 03:49:12 +00:00
Vectry
f0ce0f7884 feat: SEO improvements and npm publish prep
- Expand sitemap from 2 to 13 URLs (all docs pages)
- Update JSON-LD featureList with Anthropic, OpenCode, TypeScript SDK
- Update llms.txt with docs links, TS SDK, OpenCode plugin sections
- Add READMEs for agentlens-sdk and opencode-agentlens packages
- Add repository, homepage, author, bugs fields to both package.json
2026-02-10 03:43:04 +00:00
Vectry
434e68991d fix: include SDK and plugin workspace packages in Docker deps stage 2026-02-10 03:30:34 +00:00
Vectry
5256bf005b feat: documentation pages and updated model pricing
- Add 13 documentation pages under /docs (getting-started, concepts, SDK refs, integrations, API reference, self-hosting, OpenCode plugin)
- Shared docs layout with collapsible sidebar navigation
- Update model pricing across all SDKs: add GPT-5.x, GPT-4.1, o3/o4-mini, Claude 4.5 series, claude-opus-4-6
- Update trace-analytics context window lookup with current models
2026-02-10 03:27:11 +00:00
Vectry
6bed493275 feat: TypeScript SDK (agentlens-sdk) and OpenCode plugin (opencode-agentlens)
- packages/sdk-ts: BatchTransport, TraceBuilder, models, decision helpers
  Zero external deps, native fetch, ESM+CJS output
- packages/opencode-plugin: OpenCode plugin with hooks for:
  - Session lifecycle (create/idle/error/delete/diff)
  - Tool execution capture (before/after -> TOOL_CALL spans + TOOL_SELECTION decisions)
  - LLM call tracking (chat.message -> LLM_CALL spans with model/provider)
  - Permission flow (permission.ask -> ESCALATION decisions)
  - File edit events
  - Model cost estimation (Claude, GPT-4o, o3-mini pricing)
2026-02-10 03:08:51 +00:00
Vectry
0149e0a6f4 feat: Settings page, DELETE traces endpoint, Anthropic SDK, dashboard bug fixes
- Add /dashboard/settings with SDK connection details, data stats, purge
- Add DELETE /api/traces/[id] with cascade deletion
- Add Anthropic integration (wrap_anthropic) for Python SDK
- Fix missing root duration (totalDuration -> durationMs mapping)
- Fix truncated JSON in decision tree nodes (extract readable labels)
- Fix hardcoded 128K maxTokens in token gauge (model-aware context windows)
- Enable Settings nav item in sidebar
2026-02-10 02:35:50 +00:00
Vectry
4f7719eace fix: copy public dir in Dockerfile for llms.txt and static assets 2026-02-10 02:26:20 +00:00
Vectry
92b98f2d6f feat: Decisions page — aggregated view of all decision points across traces
Adds /dashboard/decisions page with colored type badges, search,
filters (by type), sort (newest/oldest/costliest), pagination,
and links to parent traces. New /api/decisions endpoint with
Prisma queries. Removes 'Soon' badge from sidebar nav.
2026-02-10 02:24:00 +00:00
Vectry
145b1669e7 feat: comprehensive SEO — meta tags, OG, Twitter cards, JSON-LD, sitemap, robots, llms.txt
Adds metadataBase, full OpenGraph + Twitter card tags, keywords,
JSON-LD structured data (SoftwareApplication + Organization),
sitemap.ts, robots.ts with AI crawler directives, and llms.txt
for AI agent discoverability.
2026-02-10 02:21:16 +00:00
Vectry
d91fdfc81a fix: trace ingest FK violation on tool-call decisions
Spans must be inserted before decision points due to
DecisionPoint.parentSpanId FK referencing Span.id. Switched from
nested Prisma create to interactive transaction with topological
span ordering. Also adds real MoonshotAI LLM test script.
2026-02-10 02:16:10 +00:00
55 changed files with 9057 additions and 57 deletions

View File

@@ -6,6 +6,8 @@ FROM base AS deps
COPY package.json package-lock.json* ./
COPY apps/web/package.json ./apps/web/
COPY packages/database/package.json ./packages/database/
COPY packages/sdk-ts/package.json ./packages/sdk-ts/
COPY packages/opencode-plugin/package.json ./packages/opencode-plugin/
RUN npm install --production=false
FROM base AS builder
@@ -19,6 +21,7 @@ RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
COPY --from=builder --chown=nextjs:nodejs /app/packages/database/prisma ./packages/database/prisma
USER nextjs
EXPOSE 3000

View File

@@ -18,7 +18,8 @@
"lucide-react": "^0.469.0",
"next": "^15.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"shiki": "^3.22.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.0",

69
apps/web/public/llms.txt Normal file
View File

@@ -0,0 +1,69 @@
# AgentLens
> AgentLens is an open-source agent observability platform that traces AI agent decisions, not just API calls. It captures why agents choose specific tools, routes, or strategies — providing visibility into the reasoning behind every action.
AgentLens helps engineering teams debug, monitor, and improve AI agent applications in production. Unlike traditional LLM observability tools that only trace API calls, AgentLens captures the decision-making process: tool selection rationale, routing logic, retry strategies, and planning steps. It includes a real-time dashboard with decision tree visualization, cost analytics, and token tracking.
## Getting Started
- [Documentation](https://agentlens.vectry.tech/docs): Full docs covering SDKs, integrations, API reference, and self-hosting
- [Quick Start](https://agentlens.vectry.tech/docs/getting-started): Install, initialize, and send your first trace in 5 minutes
- [GitHub Repository](https://gitea.repi.fun/repi/agentlens): Source code and issues
- [PyPI Package](https://pypi.org/project/vectry-agentlens/): Install with `pip install vectry-agentlens`
- [npm Package (SDK)](https://www.npmjs.com/package/agentlens-sdk): Install with `npm install agentlens-sdk`
- [npm Package (OpenCode Plugin)](https://www.npmjs.com/package/opencode-agentlens): Install with `npm install opencode-agentlens`
- [Dashboard](https://agentlens.vectry.tech/dashboard): Live dashboard with real-time traces
## Python SDK
- [Python SDK Reference](https://agentlens.vectry.tech/docs/python-sdk): init(), @trace decorator, log_decision(), TraceContext, configuration
- [Basic Usage](https://gitea.repi.fun/repi/agentlens/src/branch/main/examples/basic_agent.py): Minimal SDK usage with trace context and decision logging
- [OpenAI Integration](https://gitea.repi.fun/repi/agentlens/src/branch/main/examples/openai_agent.py): Wrap OpenAI client for automatic LLM call tracing
- [Multi-Agent Example](https://gitea.repi.fun/repi/agentlens/src/branch/main/examples/multi_agent.py): Nested multi-agent workflow tracing
## TypeScript SDK
- [TypeScript SDK Reference](https://agentlens.vectry.tech/docs/typescript-sdk): init(), TraceBuilder, createDecision(), BatchTransport
- Install with `npm install agentlens-sdk`
## OpenCode Plugin
- [OpenCode Plugin Docs](https://agentlens.vectry.tech/docs/opencode-plugin): Capture coding agent sessions, tool calls, LLM calls, permission flows, and file edits
- Install with `npm install opencode-agentlens`
- Configure via AGENTLENS_API_KEY and AGENTLENS_ENDPOINT environment variables
## Key Concepts
- [Concepts](https://agentlens.vectry.tech/docs/concepts): Traces, Spans, Decision Points, Events explained
- **Traces**: Top-level containers for agent execution sessions, with tags and metadata
- **Spans**: Individual operations within a trace (LLM calls, tool calls, chain steps)
- **Decision Points**: The core differentiator — captures what was chosen, what alternatives existed, and why
- **Decision Types**: TOOL_SELECTION, ROUTING, RETRY, ESCALATION, MEMORY_RETRIEVAL, PLANNING, CUSTOM
## API
- [API Reference](https://agentlens.vectry.tech/docs/api-reference): Full REST API contract
- POST /api/traces: Batch ingest traces from SDK (Bearer token auth)
- GET /api/traces: List traces with pagination, search, filters, and sorting
- GET /api/traces/:id: Get single trace with all spans, decisions, and events
- GET /api/traces/stream: Server-Sent Events for real-time trace updates
- GET /api/health: Health check endpoint
## Integrations
- [OpenAI Integration](https://agentlens.vectry.tech/docs/integrations/openai): `wrap_openai(client)` auto-instruments chat completions, streaming, and tool calls
- [Anthropic Integration](https://agentlens.vectry.tech/docs/integrations/anthropic): `wrap_anthropic(client)` auto-instruments Claude API calls
- [LangChain Integration](https://agentlens.vectry.tech/docs/integrations/langchain): `AgentLensCallbackHandler` captures chains, agents, tools, and LLM calls
- **Any Python Code**: `@trace` decorator and `log_decision()` for custom instrumentation
## Self-Hosting
- [Self-Hosting Guide](https://agentlens.vectry.tech/docs/self-hosting): Docker, docker-compose, env vars, reverse proxy
- Docker Compose deployment with PostgreSQL and Redis
- Single `docker compose up -d` to run
- Environment variables: DATABASE_URL, REDIS_URL, AGENTLENS_API_KEY
## Optional
- [Company Website](https://vectry.tech): Built by Vectry, an engineering-first AI consultancy
- [CodeBoard](https://codeboard.vectry.tech): Sister product — understand any codebase in 5 minutes

View File

@@ -0,0 +1,127 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { Prisma } from "@agentlens/database";
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get("page") ?? "1", 10);
const limit = parseInt(searchParams.get("limit") ?? "20", 10);
const type = searchParams.get("type");
const search = searchParams.get("search");
const sort = searchParams.get("sort") ?? "newest";
// Validate pagination
if (isNaN(page) || page < 1) {
return NextResponse.json(
{ error: "Invalid page parameter. Must be a positive integer." },
{ status: 400 }
);
}
if (isNaN(limit) || limit < 1 || limit > 100) {
return NextResponse.json(
{ error: "Invalid limit parameter. Must be between 1 and 100." },
{ status: 400 }
);
}
// Validate type
const validTypes = [
"TOOL_SELECTION",
"ROUTING",
"RETRY",
"ESCALATION",
"MEMORY_RETRIEVAL",
"PLANNING",
"CUSTOM",
];
if (type && !validTypes.includes(type)) {
return NextResponse.json(
{ error: `Invalid type. Must be one of: ${validTypes.join(", ")}` },
{ status: 400 }
);
}
// Validate sort
const validSorts = ["newest", "oldest", "costliest"];
if (!validSorts.includes(sort)) {
return NextResponse.json(
{ error: `Invalid sort. Must be one of: ${validSorts.join(", ")}` },
{ status: 400 }
);
}
// Build where clause
const where: Prisma.DecisionPointWhereInput = {};
if (type) {
where.type = type as Prisma.EnumDecisionTypeFilter["equals"];
}
if (search) {
where.reasoning = {
contains: search,
mode: "insensitive",
};
}
// Build order by
let orderBy: Prisma.DecisionPointOrderByWithRelationInput;
switch (sort) {
case "oldest":
orderBy = { timestamp: "asc" };
break;
case "costliest":
orderBy = { costUsd: "desc" };
break;
case "newest":
default:
orderBy = { timestamp: "desc" };
break;
}
// Count total
const total = await prisma.decisionPoint.count({ where });
// Pagination
const skip = (page - 1) * limit;
const totalPages = Math.ceil(total / limit);
// Fetch decisions with parent trace and span
const decisions = await prisma.decisionPoint.findMany({
where,
include: {
trace: {
select: {
id: true,
name: true,
},
},
span: {
select: {
id: true,
name: true,
},
},
},
orderBy,
skip,
take: limit,
});
return NextResponse.json(
{
decisions,
total,
page,
limit,
totalPages,
},
{ status: 200 }
);
} catch (error) {
console.error("Error listing decisions:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,21 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function POST() {
try {
await prisma.$transaction([
prisma.event.deleteMany(),
prisma.decisionPoint.deleteMany(),
prisma.span.deleteMany(),
prisma.trace.deleteMany(),
]);
return NextResponse.json({ success: true }, { status: 200 });
} catch (error) {
console.error("Error purging data:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function GET() {
try {
const [totalTraces, totalSpans, totalDecisions, totalEvents] =
await Promise.all([
prisma.trace.count(),
prisma.span.count(),
prisma.decisionPoint.count(),
prisma.event.count(),
]);
return NextResponse.json(
{ totalTraces, totalSpans, totalDecisions, totalEvents },
{ status: 200 }
);
} catch (error) {
console.error("Error fetching stats:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@@ -1,10 +1,26 @@
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
type RouteParams = { params: Promise<{ id: string }> };
function extractActionLabel(value: unknown): string {
if (typeof value === "string") return value;
if (value && typeof value === "object" && !Array.isArray(value)) {
const obj = value as Record<string, unknown>;
if (typeof obj.name === "string") return obj.name;
if (typeof obj.action === "string") return obj.action;
if (typeof obj.tool === "string") return obj.tool;
for (const v of Object.values(obj)) {
if (typeof v === "string") return v;
}
}
return JSON.stringify(value);
}
// GET /api/traces/[id] — Get single trace with all relations
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> }
_request: NextRequest,
{ params }: RouteParams
) {
try {
const { id } = await params;
@@ -41,11 +57,13 @@ export async function GET(
// Transform data to match frontend expectations
const transformedTrace = {
...trace,
durationMs: trace.totalDuration,
costUsd: trace.totalCost,
decisionPoints: trace.decisionPoints.map((dp) => ({
id: dp.id,
type: dp.type,
chosenAction: typeof dp.chosen === "string" ? dp.chosen : JSON.stringify(dp.chosen),
alternatives: dp.alternatives.map((alt) => (typeof alt === "string" ? alt : JSON.stringify(alt))),
chosenAction: extractActionLabel(dp.chosen),
alternatives: dp.alternatives.map((alt) => extractActionLabel(alt)),
reasoning: dp.reasoning,
contextSnapshot: dp.contextSnapshot as Record<string, unknown> | null,
confidence: null, // Not in schema, default to null
@@ -81,3 +99,35 @@ export async function GET(
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
// DELETE /api/traces/[id] — Delete a trace and all related data (cascade)
export async function DELETE(
_request: NextRequest,
{ params }: RouteParams
) {
try {
const { id } = await params;
if (!id || typeof id !== "string") {
return NextResponse.json({ error: "Invalid trace ID" }, { status: 400 });
}
const trace = await prisma.trace.findUnique({
where: { id },
select: { id: true },
});
if (!trace) {
return NextResponse.json({ error: "Trace not found" }, { status: 404 });
}
await prisma.trace.delete({
where: { id },
});
return NextResponse.json({ success: true, deleted: id }, { status: 200 });
} catch (error) {
console.error("Error deleting trace:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}

View File

@@ -63,6 +63,24 @@ interface BatchTracesRequest {
traces: TracePayload[];
}
function topologicalSortSpans(spans: SpanPayload[]): SpanPayload[] {
const byId = new Map(spans.map((s) => [s.id, s]));
const sorted: SpanPayload[] = [];
const visited = new Set<string>();
function visit(span: SpanPayload) {
if (visited.has(span.id)) return;
visited.add(span.id);
if (span.parentSpanId && byId.has(span.parentSpanId)) {
visit(byId.get(span.parentSpanId)!);
}
sorted.push(span);
}
for (const span of spans) visit(span);
return sorted;
}
// POST /api/traces — Batch ingest traces from SDK
export async function POST(request: NextRequest) {
try {
@@ -166,10 +184,15 @@ export async function POST(request: NextRequest) {
}
}
// Insert traces using transaction
const result = await prisma.$transaction(
body.traces.map((trace) =>
prisma.trace.create({
// Insert traces using interactive transaction to control insert order.
// Spans must be inserted before decision points due to the
// DecisionPoint.parentSpanId FK referencing Span.id.
const result = await prisma.$transaction(async (tx) => {
const created: string[] = [];
for (const trace of body.traces) {
// 1. Create the trace record (no nested relations)
await tx.trace.create({
data: {
id: trace.id,
name: trace.name,
@@ -182,23 +205,18 @@ export async function POST(request: NextRequest) {
totalDuration: trace.totalDuration,
startedAt: new Date(trace.startedAt),
endedAt: trace.endedAt ? new Date(trace.endedAt) : null,
decisionPoints: {
create: trace.decisionPoints.map((dp) => ({
id: dp.id,
type: dp.type,
reasoning: dp.reasoning,
chosen: dp.chosen as Prisma.InputJsonValue,
alternatives: dp.alternatives as Prisma.InputJsonValue[],
contextSnapshot: dp.contextSnapshot as Prisma.InputJsonValue | undefined,
durationMs: dp.durationMs,
costUsd: dp.costUsd,
parentSpanId: dp.parentSpanId,
timestamp: new Date(dp.timestamp),
})),
},
spans: {
create: trace.spans.map((span) => ({
},
});
// 2. Create spans FIRST (decision points may reference them via parentSpanId)
if (trace.spans.length > 0) {
// Sort spans so parents come before children
const spanOrder = topologicalSortSpans(trace.spans);
for (const span of spanOrder) {
await tx.span.create({
data: {
id: span.id,
traceId: trace.id,
parentSpanId: span.parentSpanId,
name: span.name,
type: span.type,
@@ -212,22 +230,53 @@ export async function POST(request: NextRequest) {
startedAt: new Date(span.startedAt),
endedAt: span.endedAt ? new Date(span.endedAt) : null,
metadata: span.metadata as Prisma.InputJsonValue | undefined,
})),
},
events: {
create: trace.events.map((event) => ({
id: event.id,
spanId: event.spanId,
type: event.type,
name: event.name,
metadata: event.metadata as Prisma.InputJsonValue | undefined,
timestamp: new Date(event.timestamp),
})),
},
},
})
)
);
},
});
}
}
// 3. Create decision points AFTER spans exist
if (trace.decisionPoints.length > 0) {
// Collect valid span IDs so we can null-out invalid parentSpanId refs
const validSpanIds = new Set(trace.spans.map((s) => s.id));
await tx.decisionPoint.createMany({
data: trace.decisionPoints.map((dp) => ({
id: dp.id,
traceId: trace.id,
type: dp.type,
reasoning: dp.reasoning,
chosen: dp.chosen as Prisma.InputJsonValue,
alternatives: dp.alternatives as Prisma.InputJsonValue[],
contextSnapshot: dp.contextSnapshot as Prisma.InputJsonValue | undefined,
durationMs: dp.durationMs,
costUsd: dp.costUsd,
parentSpanId: dp.parentSpanId && validSpanIds.has(dp.parentSpanId) ? dp.parentSpanId : null,
timestamp: new Date(dp.timestamp),
})),
});
}
// 4. Create events
if (trace.events.length > 0) {
await tx.event.createMany({
data: trace.events.map((event) => ({
id: event.id,
traceId: trace.id,
spanId: event.spanId,
type: event.type,
name: event.name,
metadata: event.metadata as Prisma.InputJsonValue | undefined,
timestamp: new Date(event.timestamp),
})),
});
}
created.push(trace.id);
}
return created;
});
return NextResponse.json({ success: true, count: result.length }, { status: 200 });
} catch (error) {

View File

@@ -0,0 +1,402 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import Link from "next/link";
import {
Search,
ChevronLeft,
ChevronRight,
GitBranch,
DollarSign,
Clock,
ArrowRight,
Layers,
} from "lucide-react";
import { cn, formatRelativeTime } from "@/lib/utils";
type DecisionType =
| "TOOL_SELECTION"
| "ROUTING"
| "RETRY"
| "ESCALATION"
| "MEMORY_RETRIEVAL"
| "PLANNING"
| "CUSTOM";
type SortOption = "newest" | "oldest" | "costliest";
interface Decision {
id: string;
traceId: string;
type: DecisionType;
reasoning: string | null;
chosen: Record<string, unknown>;
alternatives: Record<string, unknown>[];
contextSnapshot: Record<string, unknown> | null;
durationMs: number | null;
costUsd: number | null;
parentSpanId: string | null;
timestamp: string;
trace: {
id: string;
name: string;
};
span: {
id: string;
name: string;
} | null;
}
interface DecisionsResponse {
decisions: Decision[];
total: number;
page: number;
limit: number;
totalPages: number;
}
const typeConfig: Record<
DecisionType,
{ label: string; bg: string; text: string }
> = {
TOOL_SELECTION: {
label: "Tool Selection",
bg: "bg-blue-500/10",
text: "text-blue-400",
},
ROUTING: {
label: "Routing",
bg: "bg-purple-500/10",
text: "text-purple-400",
},
RETRY: {
label: "Retry",
bg: "bg-amber-500/10",
text: "text-amber-400",
},
ESCALATION: {
label: "Escalation",
bg: "bg-red-500/10",
text: "text-red-400",
},
MEMORY_RETRIEVAL: {
label: "Memory Retrieval",
bg: "bg-cyan-500/10",
text: "text-cyan-400",
},
PLANNING: {
label: "Planning",
bg: "bg-emerald-500/10",
text: "text-emerald-400",
},
CUSTOM: {
label: "Custom",
bg: "bg-neutral-500/10",
text: "text-neutral-400",
},
};
const sortOptions: { value: SortOption; label: string }[] = [
{ value: "newest", label: "Newest" },
{ value: "oldest", label: "Oldest" },
{ value: "costliest", label: "Most Expensive" },
];
const allTypes: DecisionType[] = [
"TOOL_SELECTION",
"ROUTING",
"RETRY",
"ESCALATION",
"MEMORY_RETRIEVAL",
"PLANNING",
"CUSTOM",
];
function extractChosenName(chosen: Record<string, unknown>): string {
if (chosen && typeof chosen === "object") {
if (typeof chosen.name === "string") return chosen.name;
if (typeof chosen.action === "string") return chosen.action;
if (typeof chosen.tool === "string") return chosen.tool;
// Fallback: first string value
for (const val of Object.values(chosen)) {
if (typeof val === "string") return val;
}
}
return "Unknown";
}
function truncate(text: string, max: number): string {
if (text.length <= max) return text;
return text.slice(0, max) + "...";
}
export default function DecisionsPage() {
const [decisions, setDecisions] = useState<Decision[]>([]);
const [total, setTotal] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [isLoading, setIsLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState("");
const [typeFilter, setTypeFilter] = useState<DecisionType | "ALL">("ALL");
const [sortFilter, setSortFilter] = useState<SortOption>("newest");
const fetchDecisions = useCallback(async () => {
setIsLoading(true);
try {
const params = new URLSearchParams();
params.set("page", currentPage.toString());
params.set("limit", "30");
if (typeFilter !== "ALL") {
params.set("type", typeFilter);
}
if (searchQuery) {
params.set("search", searchQuery);
}
if (sortFilter !== "newest") {
params.set("sort", sortFilter);
}
const res = await fetch(`/api/decisions?${params.toString()}`, {
cache: "no-store",
});
if (!res.ok) {
throw new Error(`Failed to fetch decisions: ${res.status}`);
}
const data: DecisionsResponse = await res.json();
setDecisions(data.decisions);
setTotal(data.total);
setTotalPages(data.totalPages);
} catch (error) {
console.error("Error fetching decisions:", error);
} finally {
setIsLoading(false);
}
}, [currentPage, typeFilter, searchQuery, sortFilter]);
useEffect(() => {
fetchDecisions();
}, [fetchDecisions]);
useEffect(() => {
setCurrentPage(1);
}, [searchQuery, typeFilter, sortFilter]);
const handlePageChange = (newPage: number) => {
setCurrentPage(newPage);
window.scrollTo({ top: 0, behavior: "smooth" });
};
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-neutral-100">Decisions</h1>
<p className="text-neutral-400 mt-1">
{total} decision point{total !== 1 ? "s" : ""} across all traces
</p>
</div>
{/* Search and Filters */}
<div className="flex flex-col gap-4">
<div className="flex flex-col sm:flex-row gap-4">
{/* Search */}
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-neutral-500" />
<input
type="text"
placeholder="Search by reasoning..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-3 bg-neutral-900 border border-neutral-800 rounded-lg text-neutral-100 placeholder-neutral-500 focus:outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/50 transition-all"
/>
</div>
{/* Type Filter */}
<div className="flex items-center gap-3">
<select
value={typeFilter}
onChange={(e) =>
setTypeFilter(e.target.value as DecisionType | "ALL")
}
className="bg-neutral-900 border border-neutral-800 rounded-lg px-3 py-3 text-sm text-neutral-100 focus:outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/50 transition-all"
>
<option value="ALL">All Types</option>
{allTypes.map((t) => (
<option key={t} value={t}>
{typeConfig[t].label}
</option>
))}
</select>
{/* Sort */}
<select
value={sortFilter}
onChange={(e) => setSortFilter(e.target.value as SortOption)}
className="bg-neutral-900 border border-neutral-800 rounded-lg px-3 py-3 text-sm text-neutral-100 focus:outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/50 transition-all"
>
{sortOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
</div>
{/* Loading State */}
{isLoading && decisions.length === 0 && (
<div className="space-y-3">
{Array.from({ length: 5 }).map((_, i) => (
<div
key={i}
className="p-5 bg-neutral-900 border border-neutral-800 rounded-xl animate-pulse"
>
<div className="flex items-center gap-4">
<div className="h-6 w-24 bg-neutral-800 rounded-md" />
<div className="flex-1">
<div className="h-5 w-48 bg-neutral-800 rounded-md mb-2" />
<div className="h-4 w-96 bg-neutral-800/60 rounded-md" />
</div>
<div className="h-4 w-16 bg-neutral-800 rounded-md" />
</div>
</div>
))}
</div>
)}
{/* Empty State */}
{!isLoading && decisions.length === 0 && (
<div className="flex flex-col items-center justify-center py-20 text-center">
<div className="w-20 h-20 rounded-2xl bg-neutral-900 border border-neutral-800 flex items-center justify-center mb-6">
<GitBranch className="w-10 h-10 text-neutral-600" />
</div>
<h2 className="text-xl font-semibold text-neutral-100 mb-2">
No decision points yet
</h2>
<p className="text-neutral-400 max-w-md">
Decision points will appear here once your agents start making
decisions. Send traces with decision data to see them aggregated.
</p>
</div>
)}
{/* Decision List */}
{decisions.length > 0 && (
<div className="space-y-3">
{decisions.map((decision) => (
<DecisionCard key={decision.id} decision={decision} />
))}
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between pt-6 border-t border-neutral-800">
<p className="text-sm text-neutral-500">
Page {currentPage} of {totalPages}
</p>
<div className="flex gap-2">
<button
disabled={currentPage <= 1}
onClick={() => handlePageChange(Math.max(1, currentPage - 1))}
className="p-2 rounded-lg border border-neutral-800 text-neutral-400 hover:text-neutral-100 hover:border-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronLeft className="w-5 h-5" />
</button>
<button
disabled={currentPage >= totalPages}
onClick={() =>
handlePageChange(Math.min(totalPages, currentPage + 1))
}
className="p-2 rounded-lg border border-neutral-800 text-neutral-400 hover:text-neutral-100 hover:border-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
</div>
)}
</div>
);
}
function DecisionCard({ decision }: { decision: Decision }) {
const config = typeConfig[decision.type] || typeConfig.CUSTOM;
const chosenName = extractChosenName(
decision.chosen as Record<string, unknown>
);
return (
<div className="group p-5 bg-neutral-900 border border-neutral-800 rounded-xl hover:border-emerald-500/30 hover:bg-neutral-800/50 transition-all duration-200">
<div className="flex flex-col lg:flex-row lg:items-center gap-4">
{/* Left: Type badge + chosen + reasoning */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
<span
className={cn(
"inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium uppercase tracking-wide",
config.bg,
config.text
)}
>
{config.label}
</span>
<h3 className="font-semibold text-neutral-100 truncate">
{chosenName}
</h3>
</div>
{decision.reasoning && (
<p className="text-sm text-neutral-400 leading-relaxed">
{truncate(decision.reasoning, 120)}
</p>
)}
<div className="flex items-center gap-4 mt-2 text-sm text-neutral-500">
<span className="flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5" />
{formatRelativeTime(decision.timestamp)}
</span>
{decision.alternatives.length > 0 && (
<span className="flex items-center gap-1.5">
<Layers className="w-3.5 h-3.5" />
{decision.alternatives.length} alternative
{decision.alternatives.length !== 1 ? "s" : ""}
</span>
)}
{decision.costUsd !== null && decision.costUsd > 0 && (
<span className="flex items-center gap-1.5">
<DollarSign className="w-3.5 h-3.5" />$
{decision.costUsd.toFixed(4)}
</span>
)}
{decision.span && (
<span className="flex items-center gap-1.5 text-neutral-500">
<GitBranch className="w-3.5 h-3.5" />
{decision.span.name}
</span>
)}
</div>
</div>
{/* Right: Trace link */}
<div className="flex items-center gap-3 shrink-0">
<Link
href={`/dashboard/traces/${decision.trace.id}`}
onClick={(e) => e.stopPropagation()}
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-neutral-800/50 border border-neutral-700/50 text-sm text-neutral-300 hover:text-emerald-400 hover:border-emerald-500/30 transition-all"
>
<span className="truncate max-w-[180px]">
{decision.trace.name}
</span>
<ArrowRight className="w-4 h-4 shrink-0" />
</Link>
</div>
</div>
</div>
);
}

View File

@@ -8,7 +8,6 @@ import {
GitBranch,
Settings,
Menu,
X,
ChevronRight,
} from "lucide-react";
import { cn } from "@/lib/utils";
@@ -22,8 +21,8 @@ interface NavItem {
const navItems: NavItem[] = [
{ href: "/dashboard", label: "Traces", icon: Activity },
{ href: "/dashboard/decisions", label: "Decisions", icon: GitBranch, comingSoon: true },
{ href: "/dashboard/settings", label: "Settings", icon: Settings, comingSoon: true },
{ href: "/dashboard/decisions", label: "Decisions", icon: GitBranch },
{ href: "/dashboard/settings", label: "Settings", icon: Settings },
];
function Sidebar({ onNavigate }: { onNavigate?: () => void }) {

View File

@@ -0,0 +1,294 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import {
Settings,
Key,
Globe,
Copy,
Check,
RefreshCw,
Database,
Trash2,
AlertTriangle,
} from "lucide-react";
import { cn } from "@/lib/utils";
interface Stats {
totalTraces: number;
totalSpans: number;
totalDecisions: number;
totalEvents: number;
}
export default function SettingsPage() {
const [stats, setStats] = useState<Stats | null>(null);
const [isLoadingStats, setIsLoadingStats] = useState(true);
const [copiedField, setCopiedField] = useState<string | null>(null);
const [isPurging, setIsPurging] = useState(false);
const [showPurgeConfirm, setShowPurgeConfirm] = useState(false);
const fetchStats = useCallback(async () => {
setIsLoadingStats(true);
try {
const res = await fetch("/api/settings/stats", { cache: "no-store" });
if (res.ok) {
const data = await res.json();
setStats(data);
}
} catch (error) {
console.error("Failed to fetch stats:", error);
} finally {
setIsLoadingStats(false);
}
}, []);
useEffect(() => {
fetchStats();
}, [fetchStats]);
const copyToClipboard = async (text: string, field: string) => {
try {
await navigator.clipboard.writeText(text);
setCopiedField(field);
setTimeout(() => setCopiedField(null), 2000);
} catch {
console.error("Failed to copy");
}
};
const handlePurgeAll = async () => {
setIsPurging(true);
try {
const res = await fetch("/api/settings/purge", { method: "POST" });
if (res.ok) {
setShowPurgeConfirm(false);
fetchStats();
}
} catch (error) {
console.error("Failed to purge:", error);
} finally {
setIsPurging(false);
}
};
const endpointUrl =
typeof window !== "undefined"
? `${window.location.origin}/api/traces`
: "https://agentlens.vectry.tech/api/traces";
return (
<div className="space-y-8 max-w-3xl">
<div>
<h1 className="text-2xl font-bold text-neutral-100">Settings</h1>
<p className="text-neutral-400 mt-1">
Configuration and SDK connection details
</p>
</div>
{/* SDK Connection */}
<section className="space-y-4">
<div className="flex items-center gap-2 text-neutral-300">
<Globe className="w-5 h-5 text-emerald-400" />
<h2 className="text-lg font-semibold">SDK Connection</h2>
</div>
<div className="bg-neutral-900 border border-neutral-800 rounded-xl p-6 space-y-5">
<SettingField
label="Ingest Endpoint"
value={endpointUrl}
copiedField={copiedField}
fieldKey="endpoint"
onCopy={copyToClipboard}
/>
<SettingField
label="API Key"
value="any-value-accepted"
hint="Authentication is not enforced yet. Use any non-empty string as your Bearer token."
copiedField={copiedField}
fieldKey="apikey"
onCopy={copyToClipboard}
/>
<div className="pt-4 border-t border-neutral-800">
<p className="text-xs text-neutral-500 mb-3">Quick start</p>
<div className="bg-neutral-950 border border-neutral-800 rounded-lg p-4 font-mono text-sm text-neutral-300 overflow-x-auto">
<pre>{`from agentlens import init
init(
api_key="your-api-key",
endpoint="${endpointUrl.replace("/api/traces", "")}",
)`}</pre>
</div>
</div>
</div>
</section>
{/* Data & Storage */}
<section className="space-y-4">
<div className="flex items-center gap-2 text-neutral-300">
<Database className="w-5 h-5 text-emerald-400" />
<h2 className="text-lg font-semibold">Data & Storage</h2>
</div>
<div className="bg-neutral-900 border border-neutral-800 rounded-xl p-6 space-y-5">
{isLoadingStats ? (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="animate-pulse">
<div className="h-4 w-16 bg-neutral-800 rounded mb-2" />
<div className="h-8 w-12 bg-neutral-800 rounded" />
</div>
))}
</div>
) : stats ? (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<StatCard label="Traces" value={stats.totalTraces} />
<StatCard label="Spans" value={stats.totalSpans} />
<StatCard label="Decisions" value={stats.totalDecisions} />
<StatCard label="Events" value={stats.totalEvents} />
</div>
) : (
<p className="text-sm text-neutral-500">
Unable to load statistics
</p>
)}
<div className="pt-4 border-t border-neutral-800 flex items-center justify-between">
<div>
<p className="text-sm text-neutral-300 font-medium">
Purge All Data
</p>
<p className="text-xs text-neutral-500 mt-0.5">
Permanently delete all traces, spans, decisions, and events
</p>
</div>
{showPurgeConfirm ? (
<div className="flex items-center gap-2">
<button
onClick={() => setShowPurgeConfirm(false)}
className="px-3 py-2 text-sm text-neutral-400 hover:text-neutral-200 transition-colors"
>
Cancel
</button>
<button
onClick={handlePurgeAll}
disabled={isPurging}
className="flex items-center gap-2 px-4 py-2 bg-red-500/20 border border-red-500/30 text-red-400 rounded-lg text-sm font-medium hover:bg-red-500/30 disabled:opacity-50 transition-colors"
>
{isPurging ? (
<RefreshCw className="w-4 h-4 animate-spin" />
) : (
<AlertTriangle className="w-4 h-4" />
)}
Confirm Purge
</button>
</div>
) : (
<button
onClick={() => setShowPurgeConfirm(true)}
className="flex items-center gap-2 px-4 py-2 bg-neutral-800 border border-neutral-700 text-neutral-400 rounded-lg text-sm font-medium hover:text-red-400 hover:border-red-500/30 transition-colors"
>
<Trash2 className="w-4 h-4" />
Purge
</button>
)}
</div>
</div>
</section>
{/* About */}
<section className="space-y-4">
<div className="flex items-center gap-2 text-neutral-300">
<Settings className="w-5 h-5 text-emerald-400" />
<h2 className="text-lg font-semibold">About</h2>
</div>
<div className="bg-neutral-900 border border-neutral-800 rounded-xl p-6">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-neutral-500">Version</p>
<p className="text-neutral-200 font-medium">0.1.0</p>
</div>
<div>
<p className="text-neutral-500">SDK Package</p>
<p className="text-neutral-200 font-medium">vectry-agentlens</p>
</div>
<div>
<p className="text-neutral-500">Database</p>
<p className="text-neutral-200 font-medium">PostgreSQL</p>
</div>
<div>
<p className="text-neutral-500">License</p>
<p className="text-neutral-200 font-medium">MIT</p>
</div>
</div>
</div>
</section>
</div>
);
}
function SettingField({
label,
value,
hint,
copiedField,
fieldKey,
onCopy,
}: {
label: string;
value: string;
hint?: string;
copiedField: string | null;
fieldKey: string;
onCopy: (text: string, field: string) => void;
}) {
const isCopied = copiedField === fieldKey;
return (
<div>
<label className="text-xs text-neutral-500 font-medium block mb-1.5">
{label}
</label>
<div className="flex items-center gap-2">
<div className="flex-1 flex items-center gap-2 px-3 py-2.5 bg-neutral-950 border border-neutral-800 rounded-lg">
<Key className="w-4 h-4 text-neutral-600 shrink-0" />
<code className="text-sm text-neutral-300 font-mono truncate">
{value}
</code>
</div>
<button
onClick={() => onCopy(value, fieldKey)}
className={cn(
"p-2.5 rounded-lg border transition-all",
isCopied
? "bg-emerald-500/10 border-emerald-500/30 text-emerald-400"
: "bg-neutral-800 border-neutral-700 text-neutral-400 hover:text-neutral-200"
)}
>
{isCopied ? (
<Check className="w-4 h-4" />
) : (
<Copy className="w-4 h-4" />
)}
</button>
</div>
{hint && (
<p className="text-xs text-neutral-600 mt-1.5">{hint}</p>
)}
</div>
);
}
function StatCard({ label, value }: { label: string; value: number }) {
return (
<div className="p-3 bg-neutral-800/50 rounded-lg">
<p className="text-xs text-neutral-500">{label}</p>
<p className="text-xl font-bold text-neutral-100 mt-1">
{value.toLocaleString()}
</p>
</div>
);
}

View File

@@ -0,0 +1,615 @@
import type { Metadata } from "next";
import { CodeBlock } from "@/components/code-block";
export const metadata: Metadata = {
title: "REST API Reference",
description:
"Complete API contract for AgentLens trace ingestion and retrieval endpoints.",
};
function EndpointHeader({
method,
path,
description,
}: {
method: string;
path: string;
description: string;
}) {
const methodColor =
method === "POST"
? "bg-amber-500/10 text-amber-400 border-amber-500/20"
: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20";
return (
<div className="mb-6">
<div className="flex items-center gap-3 mb-2">
<span
className={`px-2.5 py-1 rounded text-xs font-mono font-bold border ${methodColor}`}
>
{method}
</span>
<code className="text-lg font-mono text-neutral-200">{path}</code>
</div>
<p className="text-neutral-400">{description}</p>
</div>
);
}
export default function ApiReferencePage() {
return (
<div>
<h1 className="text-4xl font-bold tracking-tight mb-4">
REST API Reference
</h1>
<p className="text-lg text-neutral-400 mb-4 leading-relaxed">
The AgentLens REST API is used by the SDKs to ingest and retrieve
traces. You can also call it directly for custom integrations.
</p>
<div className="px-4 py-3 rounded-lg bg-neutral-900/50 border border-neutral-800/50 mb-10">
<span className="text-sm text-neutral-400">Base URL: </span>
<code className="text-sm font-mono text-emerald-400">
https://agentlens.vectry.tech
</code>
</div>
<section className="mb-6">
<h2 className="text-2xl font-semibold mb-4">Authentication</h2>
<p className="text-neutral-400 leading-relaxed mb-4">
All write endpoints require a Bearer token in the Authorization header:
</p>
<CodeBlock>{`Authorization: Bearer your-api-key`}</CodeBlock>
</section>
<hr className="border-neutral-800/50 my-10" />
<section className="mb-12">
<EndpointHeader
method="POST"
path="/api/traces"
description="Batch ingest one or more traces with their spans, decision points, and events."
/>
<h3 className="text-lg font-medium text-neutral-200 mb-3">
Request body
</h3>
<CodeBlock title="request_body.json" language="json">{`{
"traces": [
{
"id": "trace-uuid-v4",
"name": "my-agent-run",
"sessionId": "session-abc",
"status": "COMPLETED",
"tags": ["production", "v2"],
"metadata": { "user_id": "u-123" },
"totalCost": 0.045,
"totalTokens": 1500,
"totalDuration": 3200,
"startedAt": "2026-01-15T10:00:00.000Z",
"endedAt": "2026-01-15T10:00:03.200Z",
"spans": [ ... ],
"decisionPoints": [ ... ],
"events": [ ... ]
}
]
}`}</CodeBlock>
<h3 className="text-lg font-medium text-neutral-200 mb-3 mt-8">
TracePayload
</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Field</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Type</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Required</th>
<th className="text-left py-2 text-neutral-400 font-medium">Description</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">id</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">UUID v4 unique identifier</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">name</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">Human-readable trace name</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">sessionId</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Group traces into a session</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">status</td>
<td className="py-2 pr-4 text-neutral-500">enum</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">RUNNING | COMPLETED | ERROR</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">tags</td>
<td className="py-2 pr-4 text-neutral-500">string[]</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">Array of tag strings</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">metadata</td>
<td className="py-2 pr-4 text-neutral-500">object</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Arbitrary JSON metadata</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">totalCost</td>
<td className="py-2 pr-4 text-neutral-500">number</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Total cost in USD</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">totalTokens</td>
<td className="py-2 pr-4 text-neutral-500">number</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Total token count</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">totalDuration</td>
<td className="py-2 pr-4 text-neutral-500">number</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Total duration in milliseconds</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">startedAt</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">ISO 8601 datetime</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">endedAt</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">ISO 8601 datetime (null if RUNNING)</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">spans</td>
<td className="py-2 pr-4 text-neutral-500">SpanPayload[]</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">Array of spans (can be empty)</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">decisionPoints</td>
<td className="py-2 pr-4 text-neutral-500">DecisionPointPayload[]</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">Array of decision points (can be empty)</td>
</tr>
<tr>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">events</td>
<td className="py-2 pr-4 text-neutral-500">EventPayload[]</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">Array of events (can be empty)</td>
</tr>
</tbody>
</table>
</div>
<h3 className="text-lg font-medium text-neutral-200 mb-3 mt-8">
SpanPayload
</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Field</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Type</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Required</th>
<th className="text-left py-2 text-neutral-400 font-medium">Description</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">id</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">UUID v4</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">parentSpanId</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Parent span ID for nesting</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">name</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">Span name</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">type</td>
<td className="py-2 pr-4 text-neutral-500">enum</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">LLM_CALL | TOOL_CALL | MEMORY_OP | CHAIN | AGENT | CUSTOM</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">input</td>
<td className="py-2 pr-4 text-neutral-500">object</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">JSON input payload</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">output</td>
<td className="py-2 pr-4 text-neutral-500">object</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">JSON output payload</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">tokenCount</td>
<td className="py-2 pr-4 text-neutral-500">number</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Total tokens</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">costUsd</td>
<td className="py-2 pr-4 text-neutral-500">number</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Cost in USD</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">durationMs</td>
<td className="py-2 pr-4 text-neutral-500">number</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Duration in milliseconds</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">status</td>
<td className="py-2 pr-4 text-neutral-500">enum</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">RUNNING | COMPLETED | ERROR</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">statusMessage</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Error message or status description</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">startedAt</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">ISO 8601 datetime</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">endedAt</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">ISO 8601 datetime</td>
</tr>
<tr>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">metadata</td>
<td className="py-2 pr-4 text-neutral-500">object</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Arbitrary JSON metadata</td>
</tr>
</tbody>
</table>
</div>
<h3 className="text-lg font-medium text-neutral-200 mb-3 mt-8">
DecisionPointPayload
</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Field</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Type</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Required</th>
<th className="text-left py-2 text-neutral-400 font-medium">Description</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">id</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">UUID v4</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">type</td>
<td className="py-2 pr-4 text-neutral-500">enum</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">TOOL_SELECTION | ROUTING | RETRY | ESCALATION | MEMORY_RETRIEVAL | PLANNING | CUSTOM</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">reasoning</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Why this choice was made</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">chosen</td>
<td className="py-2 pr-4 text-neutral-500">object</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">JSON value representing the choice made</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">alternatives</td>
<td className="py-2 pr-4 text-neutral-500">object[]</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">Array of alternatives considered</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">contextSnapshot</td>
<td className="py-2 pr-4 text-neutral-500">object</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Context at decision time</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">durationMs</td>
<td className="py-2 pr-4 text-neutral-500">number</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Decision time in milliseconds</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">costUsd</td>
<td className="py-2 pr-4 text-neutral-500">number</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Cost of this decision</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">parentSpanId</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Span this decision belongs to</td>
</tr>
<tr>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">timestamp</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">ISO 8601 datetime</td>
</tr>
</tbody>
</table>
</div>
<h3 className="text-lg font-medium text-neutral-200 mb-3 mt-8">
EventPayload
</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Field</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Type</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Required</th>
<th className="text-left py-2 text-neutral-400 font-medium">Description</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">id</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">UUID v4</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">spanId</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Span this event is associated with</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">type</td>
<td className="py-2 pr-4 text-neutral-500">enum</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">ERROR | RETRY | FALLBACK | CONTEXT_OVERFLOW | USER_FEEDBACK | CUSTOM</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">name</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">Human-readable event name</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">metadata</td>
<td className="py-2 pr-4 text-neutral-500">object</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Arbitrary JSON metadata</td>
</tr>
<tr>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">timestamp</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">ISO 8601 datetime</td>
</tr>
</tbody>
</table>
</div>
<h3 className="text-lg font-medium text-neutral-200 mb-3 mt-8">
Responses
</h3>
<div className="space-y-3">
<div className="p-3 rounded-lg border border-neutral-800/50 bg-neutral-900/30 flex items-start gap-3">
<span className="px-2 py-0.5 rounded text-xs font-mono font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 flex-shrink-0">
200
</span>
<div>
<p className="text-sm text-neutral-300">Success</p>
<code className="text-xs font-mono text-neutral-500">{`{ "success": true, "count": 1 }`}</code>
</div>
</div>
<div className="p-3 rounded-lg border border-neutral-800/50 bg-neutral-900/30 flex items-start gap-3">
<span className="px-2 py-0.5 rounded text-xs font-mono font-bold bg-amber-500/10 text-amber-400 border border-amber-500/20 flex-shrink-0">
400
</span>
<div>
<p className="text-sm text-neutral-300">Bad Request</p>
<code className="text-xs font-mono text-neutral-500">{`{ "error": "Request body must contain a 'traces' array" }`}</code>
</div>
</div>
<div className="p-3 rounded-lg border border-neutral-800/50 bg-neutral-900/30 flex items-start gap-3">
<span className="px-2 py-0.5 rounded text-xs font-mono font-bold bg-red-500/10 text-red-400 border border-red-500/20 flex-shrink-0">
401
</span>
<div>
<p className="text-sm text-neutral-300">Unauthorized</p>
<code className="text-xs font-mono text-neutral-500">{`{ "error": "Missing or invalid Authorization header" }`}</code>
</div>
</div>
<div className="p-3 rounded-lg border border-neutral-800/50 bg-neutral-900/30 flex items-start gap-3">
<span className="px-2 py-0.5 rounded text-xs font-mono font-bold bg-amber-500/10 text-amber-400 border border-amber-500/20 flex-shrink-0">
409
</span>
<div>
<p className="text-sm text-neutral-300">Conflict</p>
<code className="text-xs font-mono text-neutral-500">{`{ "error": "Duplicate trace ID detected" }`}</code>
</div>
</div>
<div className="p-3 rounded-lg border border-neutral-800/50 bg-neutral-900/30 flex items-start gap-3">
<span className="px-2 py-0.5 rounded text-xs font-mono font-bold bg-red-500/10 text-red-400 border border-red-500/20 flex-shrink-0">
500
</span>
<div>
<p className="text-sm text-neutral-300">Internal Server Error</p>
<code className="text-xs font-mono text-neutral-500">{`{ "error": "Internal server error" }`}</code>
</div>
</div>
</div>
<h3 className="text-lg font-medium text-neutral-200 mb-3 mt-8">
cURL example
</h3>
<CodeBlock title="terminal" language="bash">{`curl -X POST https://agentlens.vectry.tech/api/traces \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer your-api-key" \\
-d '{
"traces": [{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "test-trace",
"status": "COMPLETED",
"tags": ["test"],
"startedAt": "2026-01-15T10:00:00.000Z",
"endedAt": "2026-01-15T10:00:01.000Z",
"spans": [],
"decisionPoints": [],
"events": []
}]
}'`}</CodeBlock>
</section>
<hr className="border-neutral-800/50 my-10" />
<section>
<EndpointHeader
method="GET"
path="/api/traces"
description="List traces with pagination, filtering, and sorting."
/>
<h3 className="text-lg font-medium text-neutral-200 mb-3">
Query parameters
</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Param</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Type</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Default</th>
<th className="text-left py-2 text-neutral-400 font-medium">Description</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">page</td>
<td className="py-2 pr-4 text-neutral-500">integer</td>
<td className="py-2 pr-4 text-neutral-500">1</td>
<td className="py-2">Page number (1-based)</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">limit</td>
<td className="py-2 pr-4 text-neutral-500">integer</td>
<td className="py-2 pr-4 text-neutral-500">20</td>
<td className="py-2">Results per page (1-100)</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">status</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">-</td>
<td className="py-2">Filter by status: RUNNING, COMPLETED, ERROR</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">search</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">-</td>
<td className="py-2">Case-insensitive search on trace name</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">sessionId</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">-</td>
<td className="py-2">Filter by session ID</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">tags</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">-</td>
<td className="py-2">Comma-separated tags (matches any)</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">sort</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">newest</td>
<td className="py-2">newest, oldest, longest, shortest, costliest</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">dateFrom</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">-</td>
<td className="py-2">ISO 8601 lower bound</td>
</tr>
<tr>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">dateTo</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">-</td>
<td className="py-2">ISO 8601 upper bound</td>
</tr>
</tbody>
</table>
</div>
<h3 className="text-lg font-medium text-neutral-200 mb-3 mt-8">
Response shape
</h3>
<CodeBlock title="response.json" language="json">{`{
"traces": [
{
"id": "...",
"name": "my-agent",
"status": "COMPLETED",
"tags": ["production"],
"startedAt": "2026-01-15T10:00:00.000Z",
"endedAt": "2026-01-15T10:00:03.200Z",
"totalCost": 0.045,
"totalTokens": 1500,
"totalDuration": 3200,
"_count": {
"decisionPoints": 3,
"spans": 7,
"events": 1
}
}
],
"total": 142,
"page": 1,
"limit": 20,
"totalPages": 8
}`}</CodeBlock>
</section>
</div>
);
}

View File

@@ -0,0 +1,265 @@
import type { Metadata } from "next";
import { CodeBlock } from "@/components/code-block";
export const metadata: Metadata = {
title: "Core Concepts",
description:
"Understand the four core data types in AgentLens: Traces, Spans, Decision Points, and Events.",
};
function ConceptCard({
title,
description,
children,
}: {
title: string;
description: string;
children: React.ReactNode;
}) {
return (
<div className="mb-12 pb-12 border-b border-neutral-800/50 last:border-0">
<h2 className="text-2xl font-semibold mb-3">{title}</h2>
<p className="text-neutral-400 leading-relaxed mb-6">{description}</p>
{children}
</div>
);
}
export default function ConceptsPage() {
return (
<div>
<h1 className="text-4xl font-bold tracking-tight mb-4">
Core Concepts
</h1>
<p className="text-lg text-neutral-400 mb-10 leading-relaxed">
AgentLens organizes observability data into four core types. Together
they give you a complete picture of what your agents do and why.
</p>
<ConceptCard
title="Trace"
description="A Trace is the top-level container for a single agent execution. It groups all the work that happens from the moment your agent starts until it finishes. Every span, decision point, and event belongs to exactly one trace."
>
<h3 className="text-base font-medium text-neutral-200 mb-3">
Properties
</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">
Field
</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">
Type
</th>
<th className="text-left py-2 text-neutral-400 font-medium">
Description
</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">id</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2">Unique identifier (UUID v4)</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">name</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2">Human-readable label for the trace</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">status</td>
<td className="py-2 pr-4 text-neutral-500">enum</td>
<td className="py-2">RUNNING, COMPLETED, or ERROR</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">tags</td>
<td className="py-2 pr-4 text-neutral-500">string[]</td>
<td className="py-2">Freeform labels for filtering</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">sessionId</td>
<td className="py-2 pr-4 text-neutral-500">string?</td>
<td className="py-2">Groups traces from the same session</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">startedAt</td>
<td className="py-2 pr-4 text-neutral-500">ISO datetime</td>
<td className="py-2">When the trace began</td>
</tr>
<tr>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">endedAt</td>
<td className="py-2 pr-4 text-neutral-500">ISO datetime?</td>
<td className="py-2">When the trace finished (null if still running)</td>
</tr>
</tbody>
</table>
</div>
</ConceptCard>
<ConceptCard
title="Span"
description="A Span represents a unit of work within a trace. Spans form a tree: each span can have a parent, creating a hierarchy that shows how work is nested. For example, an AGENT span may contain several LLM_CALL and TOOL_CALL child spans."
>
<h3 className="text-base font-medium text-neutral-200 mb-3">
Span Types
</h3>
<div className="grid sm:grid-cols-2 gap-3 mb-6">
{[
{ type: "LLM_CALL", desc: "A call to a language model (OpenAI, Anthropic, etc.)" },
{ type: "TOOL_CALL", desc: "An invocation of an external tool or function" },
{ type: "MEMORY_OP", desc: "A read or write to a vector store or memory system" },
{ type: "CHAIN", desc: "A sequential pipeline of operations" },
{ type: "AGENT", desc: "A top-level agent or sub-agent execution" },
{ type: "CUSTOM", desc: "Any user-defined operation type" },
].map((item) => (
<div
key={item.type}
className="p-3 rounded-lg border border-neutral-800/50 bg-neutral-900/30"
>
<span className="font-mono text-xs text-emerald-400">
{item.type}
</span>
<p className="text-xs text-neutral-500 mt-1">{item.desc}</p>
</div>
))}
</div>
<h3 className="text-base font-medium text-neutral-200 mb-3">
Nesting example
</h3>
<CodeBlock>{`Trace: "research-agent"
Span: "agent" (AGENT)
Span: "plan" (LLM_CALL)
Span: "web-search" (TOOL_CALL)
Span: "summarize" (LLM_CALL)`}</CodeBlock>
<h3 className="text-base font-medium text-neutral-200 mb-3 mt-6">
Key properties
</h3>
<ul className="text-sm text-neutral-400 space-y-2 ml-1">
<li>
<span className="font-mono text-emerald-400 text-xs">input</span> / <span className="font-mono text-emerald-400 text-xs">output</span> JSON payloads capturing what went in and came out
</li>
<li>
<span className="font-mono text-emerald-400 text-xs">tokenCount</span> Total tokens consumed (for LLM_CALL spans)
</li>
<li>
<span className="font-mono text-emerald-400 text-xs">costUsd</span> Dollar cost of this span
</li>
<li>
<span className="font-mono text-emerald-400 text-xs">durationMs</span> Wall-clock time in milliseconds
</li>
<li>
<span className="font-mono text-emerald-400 text-xs">parentSpanId</span> Reference to the parent span (null for root spans)
</li>
</ul>
</ConceptCard>
<ConceptCard
title="Decision Point"
description="A Decision Point records where your agent chose between alternatives. This is what separates AgentLens from generic tracing tools: you see the reasoning, what was chosen, and what was considered but rejected."
>
<h3 className="text-base font-medium text-neutral-200 mb-3">
Decision Point Types
</h3>
<div className="grid sm:grid-cols-2 gap-3 mb-6">
{[
{ type: "TOOL_SELECTION", desc: "Agent chose which tool to call" },
{ type: "ROUTING", desc: "Agent routed to a specific sub-agent or branch" },
{ type: "RETRY", desc: "Agent decided to retry a failed operation" },
{ type: "ESCALATION", desc: "Agent escalated to a human or higher-level agent" },
{ type: "MEMORY_RETRIEVAL", desc: "Agent chose what context to retrieve" },
{ type: "PLANNING", desc: "Agent formulated a multi-step plan" },
].map((item) => (
<div
key={item.type}
className="p-3 rounded-lg border border-neutral-800/50 bg-neutral-900/30"
>
<span className="font-mono text-xs text-emerald-400">
{item.type}
</span>
<p className="text-xs text-neutral-500 mt-1">{item.desc}</p>
</div>
))}
</div>
<h3 className="text-base font-medium text-neutral-200 mb-3">
Structure
</h3>
<CodeBlock title="decision_point.json" language="json">{`{
"type": "TOOL_SELECTION",
"reasoning": "User asked about weather, need real-time data",
"chosen": { "tool": "weather_api", "confidence": 0.95 },
"alternatives": [
{ "tool": "web_search", "confidence": 0.72 },
{ "tool": "knowledge_base", "confidence": 0.31 }
],
"contextSnapshot": { "user_intent": "weather_query" }
}`}</CodeBlock>
</ConceptCard>
<ConceptCard
title="Event"
description="An Event is a discrete occurrence during a trace that does not represent a unit of work but is worth recording. Events capture errors, retries, fallbacks, and other notable moments."
>
<h3 className="text-base font-medium text-neutral-200 mb-3">
Event Types
</h3>
<div className="grid sm:grid-cols-2 gap-3 mb-6">
{[
{ type: "ERROR", desc: "An exception or failure occurred" },
{ type: "RETRY", desc: "An operation was retried" },
{ type: "FALLBACK", desc: "A fallback path was triggered" },
{ type: "CONTEXT_OVERFLOW", desc: "Context window limit was exceeded" },
{ type: "USER_FEEDBACK", desc: "User provided feedback on an output" },
{ type: "CUSTOM", desc: "Any user-defined event type" },
].map((item) => (
<div
key={item.type}
className="p-3 rounded-lg border border-neutral-800/50 bg-neutral-900/30"
>
<span className="font-mono text-xs text-emerald-400">
{item.type}
</span>
<p className="text-xs text-neutral-500 mt-1">{item.desc}</p>
</div>
))}
</div>
<h3 className="text-base font-medium text-neutral-200 mb-3">
Example
</h3>
<CodeBlock title="event.json" language="json">{`{
"type": "CONTEXT_OVERFLOW",
"name": "token-limit-exceeded",
"metadata": {
"limit": 128000,
"actual": 131072,
"truncated_chars": 4200
},
"timestamp": "2026-01-15T10:30:00.000Z"
}`}</CodeBlock>
</ConceptCard>
<section>
<h2 className="text-2xl font-semibold mb-4">How they fit together</h2>
<CodeBlock>{`Trace: "customer-support-agent"
|
+-- Span: "classify-intent" (LLM_CALL)
| Decision: ROUTING -> chose "refund-flow" over "faq-flow"
|
+-- Span: "refund-flow" (AGENT)
| +-- Span: "lookup-order" (TOOL_CALL)
| +-- Span: "process-refund" (TOOL_CALL)
| Event: ERROR -> "payment-gateway-timeout"
| Event: RETRY -> "retrying with backup gateway"
| +-- Span: "process-refund-retry" (TOOL_CALL)
|
+-- Span: "compose-response" (LLM_CALL)`}</CodeBlock>
</section>
</div>
);
}

View File

@@ -0,0 +1,121 @@
"use client";
import { useState } from "react";
interface NavItem {
title: string;
href: string;
}
interface NavSection {
heading: string;
items: NavItem[];
}
const navigation: NavSection[] = [
{
heading: "Overview",
items: [
{ title: "Introduction", href: "/docs" },
{ title: "Getting Started", href: "/docs/getting-started" },
{ title: "Core Concepts", href: "/docs/concepts" },
],
},
{
heading: "SDKs",
items: [
{ title: "Python SDK", href: "/docs/python-sdk" },
{ title: "TypeScript SDK", href: "/docs/typescript-sdk" },
],
},
{
heading: "Integrations",
items: [
{ title: "OpenAI", href: "/docs/integrations/openai" },
{ title: "Anthropic", href: "/docs/integrations/anthropic" },
{ title: "LangChain", href: "/docs/integrations/langchain" },
],
},
{
heading: "Tools",
items: [{ title: "OpenCode Plugin", href: "/docs/opencode-plugin" }],
},
{
heading: "Reference",
items: [
{ title: "REST API", href: "/docs/api-reference" },
{ title: "Self-Hosting", href: "/docs/self-hosting" },
],
},
];
function SidebarContent() {
return (
<nav className="space-y-6">
{navigation.map((section) => (
<div key={section.heading}>
<h4 className="text-xs font-semibold uppercase tracking-wider text-neutral-500 mb-2 px-3">
{section.heading}
</h4>
<ul className="space-y-0.5">
{section.items.map((item) => (
<li key={item.href}>
<a
href={item.href}
className="block px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100 hover:bg-neutral-800/50 rounded-md transition-colors"
>
{item.title}
</a>
</li>
))}
</ul>
</div>
))}
</nav>
);
}
export function DocsSidebar() {
const [mobileOpen, setMobileOpen] = useState(false);
return (
<>
<button
type="button"
onClick={() => setMobileOpen(!mobileOpen)}
className="lg:hidden fixed bottom-4 right-4 z-50 w-12 h-12 rounded-full bg-emerald-500 text-neutral-950 shadow-lg shadow-emerald-500/25 flex items-center justify-center"
aria-label="Toggle navigation"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
{mobileOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
{mobileOpen && (
<div className="lg:hidden fixed inset-0 z-40">
<div
className="absolute inset-0 bg-neutral-950/80 backdrop-blur-sm"
onClick={() => setMobileOpen(false)}
onKeyDown={(e) => {
if (e.key === "Escape") setMobileOpen(false);
}}
role="button"
tabIndex={0}
aria-label="Close navigation"
/>
<div className="absolute left-0 top-14 bottom-0 w-72 bg-neutral-950 border-r border-neutral-800/50 p-6 overflow-y-auto">
<SidebarContent />
</div>
</div>
)}
<aside className="hidden lg:block w-64 flex-shrink-0 border-r border-neutral-800/50 sticky top-14 h-[calc(100vh-3.5rem)] overflow-y-auto py-8 px-4">
<SidebarContent />
</aside>
</>
);
}

View File

@@ -0,0 +1,212 @@
import type { Metadata } from "next";
import { CodeBlock } from "@/components/code-block";
export const metadata: Metadata = {
title: "Getting Started",
description:
"Install AgentLens, initialize the SDK, and send your first trace in under five minutes.",
};
export default function GettingStartedPage() {
return (
<div>
<h1 className="text-4xl font-bold tracking-tight mb-4">
Getting Started
</h1>
<p className="text-lg text-neutral-400 mb-10 leading-relaxed">
Go from zero to full agent observability in under five minutes. This
guide walks you through installing the SDK, initializing it, and sending
your first trace.
</p>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Prerequisites</h2>
<ul className="list-disc list-inside text-neutral-400 space-y-2 ml-1">
<li>Python 3.9+ or Node.js 18+</li>
<li>
An AgentLens instance (use{" "}
<a
href="https://agentlens.vectry.tech"
className="text-emerald-400 hover:underline"
>
agentlens.vectry.tech
</a>{" "}
or{" "}
<a
href="/docs/self-hosting"
className="text-emerald-400 hover:underline"
>
self-host
</a>
)
</li>
<li>An API key for authentication</li>
</ul>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">
Step 1: Install the SDK
</h2>
<h3 className="text-lg font-medium text-neutral-200 mb-2">Python</h3>
<CodeBlock title="terminal" language="bash">pip install vectry-agentlens</CodeBlock>
<h3 className="text-lg font-medium text-neutral-200 mb-2 mt-6">
TypeScript / Node.js
</h3>
<CodeBlock title="terminal" language="bash">npm install agentlens-sdk</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">
Step 2: Initialize AgentLens
</h2>
<h3 className="text-lg font-medium text-neutral-200 mb-2">Python</h3>
<CodeBlock title="main.py" language="python">{`import agentlens
agentlens.init(
api_key="your-api-key",
endpoint="https://agentlens.vectry.tech"
)`}</CodeBlock>
<h3 className="text-lg font-medium text-neutral-200 mb-2 mt-6">
TypeScript
</h3>
<CodeBlock title="index.ts" language="typescript">{`import { init } from "agentlens-sdk";
init({
apiKey: "your-api-key",
endpoint: "https://agentlens.vectry.tech",
});`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">
Step 3: Trace your first agent
</h2>
<h3 className="text-lg font-medium text-neutral-200 mb-2">Python</h3>
<CodeBlock title="agent.py" language="python">{`import agentlens
from agentlens import trace
agentlens.init(
api_key="your-api-key",
endpoint="https://agentlens.vectry.tech"
)
@trace(name="my-first-agent")
def my_agent(prompt: str) -> str:
# Your agent logic here
response = call_llm(prompt)
return response
# Run it — the trace is sent automatically
result = my_agent("What is the capital of France?")`}</CodeBlock>
<h3 className="text-lg font-medium text-neutral-200 mb-2 mt-6">
TypeScript
</h3>
<CodeBlock title="agent.ts" language="typescript">{`import { init, TraceBuilder } from "agentlens-sdk";
init({
apiKey: "your-api-key",
endpoint: "https://agentlens.vectry.tech",
});
const trace = new TraceBuilder("my-first-agent");
trace.addSpan({
name: "llm-call",
type: "LLM_CALL",
input: { prompt: "What is the capital of France?" },
output: { response: "Paris" },
status: "COMPLETED",
});
await trace.end();`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">
Step 4: View in the dashboard
</h2>
<p className="text-neutral-400 leading-relaxed mb-4">
Open your AgentLens dashboard to see the trace you just sent. You will
see the trace name, its status, timing information, and any spans or
decision points you recorded.
</p>
<a
href="/dashboard"
className="inline-flex items-center gap-2 px-5 py-2.5 bg-emerald-500 hover:bg-emerald-400 text-neutral-950 font-semibold rounded-lg transition-colors text-sm"
>
Open Dashboard
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13 7l5 5m0 0l-5 5m5-5H6"
/>
</svg>
</a>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Next steps</h2>
<div className="grid sm:grid-cols-2 gap-4">
<a
href="/docs/concepts"
className="group block p-4 rounded-xl border border-neutral-800/50 hover:border-emerald-500/30 transition-colors"
>
<h3 className="text-sm font-semibold text-neutral-200 group-hover:text-emerald-400 transition-colors">
Core Concepts
</h3>
<p className="text-xs text-neutral-500 mt-1">
Learn about Traces, Spans, Decision Points, and Events.
</p>
</a>
<a
href="/docs/python-sdk"
className="group block p-4 rounded-xl border border-neutral-800/50 hover:border-emerald-500/30 transition-colors"
>
<h3 className="text-sm font-semibold text-neutral-200 group-hover:text-emerald-400 transition-colors">
Python SDK Reference
</h3>
<p className="text-xs text-neutral-500 mt-1">
Explore the full Python SDK API surface.
</p>
</a>
<a
href="/docs/integrations/openai"
className="group block p-4 rounded-xl border border-neutral-800/50 hover:border-emerald-500/30 transition-colors"
>
<h3 className="text-sm font-semibold text-neutral-200 group-hover:text-emerald-400 transition-colors">
OpenAI Integration
</h3>
<p className="text-xs text-neutral-500 mt-1">
Auto-trace OpenAI calls with a single wrapper.
</p>
</a>
<a
href="/docs/self-hosting"
className="group block p-4 rounded-xl border border-neutral-800/50 hover:border-emerald-500/30 transition-colors"
>
<h3 className="text-sm font-semibold text-neutral-200 group-hover:text-emerald-400 transition-colors">
Self-Hosting
</h3>
<p className="text-xs text-neutral-500 mt-1">
Deploy your own AgentLens instance with Docker.
</p>
</a>
</div>
</section>
</div>
);
}

View File

@@ -0,0 +1,177 @@
import type { Metadata } from "next";
import { CodeBlock } from "@/components/code-block";
export const metadata: Metadata = {
title: "Anthropic Integration",
description:
"Wrap the Anthropic client to automatically trace Claude API calls with full metadata capture.",
};
export default function AnthropicIntegrationPage() {
return (
<div>
<h1 className="text-4xl font-bold tracking-tight mb-4">
Anthropic Integration
</h1>
<p className="text-lg text-neutral-400 mb-10 leading-relaxed">
Wrap the Anthropic Python client to automatically trace all Claude API
calls. AgentLens captures model, token usage, cost, latency, and the
full message exchange.
</p>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Installation</h2>
<CodeBlock title="terminal" language="bash">{`pip install vectry-agentlens anthropic`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Quick setup</h2>
<CodeBlock title="main.py" language="python">{`import agentlens
from agentlens.integrations.anthropic import wrap_anthropic
import anthropic
agentlens.init(
api_key="your-api-key",
endpoint="https://agentlens.vectry.tech",
)
client = wrap_anthropic(anthropic.Anthropic())
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[
{"role": "user", "content": "Explain the halting problem."},
],
)`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">What gets captured</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Field</th>
<th className="text-left py-2 text-neutral-400 font-medium">Description</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">input.model</td>
<td className="py-2">Model name (claude-sonnet-4-20250514, claude-haiku, etc.)</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">input.messages</td>
<td className="py-2">Full message array sent to the API</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">input.system</td>
<td className="py-2">System prompt if provided</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">output.content</td>
<td className="py-2">Response content blocks</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">tokenCount</td>
<td className="py-2">Input tokens + output tokens</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">costUsd</td>
<td className="py-2">Estimated cost based on model pricing</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">durationMs</td>
<td className="py-2">Wall-clock request time</td>
</tr>
<tr>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">metadata.stop_reason</td>
<td className="py-2">How generation ended (end_turn, max_tokens, tool_use)</td>
</tr>
</tbody>
</table>
</div>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Async client</h2>
<CodeBlock title="async_example.py" language="python">{`from agentlens.integrations.anthropic import wrap_anthropic
import anthropic
async_client = wrap_anthropic(anthropic.AsyncAnthropic())
response = await async_client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}],
)`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">
Combining with @trace
</h2>
<CodeBlock title="combined.py" language="python">{`import agentlens
from agentlens import trace
from agentlens.integrations.anthropic import wrap_anthropic
import anthropic
agentlens.init(api_key="...", endpoint="...")
client = wrap_anthropic(anthropic.Anthropic())
@trace(name="analysis-agent")
async def analyze(document: str) -> str:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
system="You are a document analysis expert.",
messages=[{"role": "user", "content": f"Analyze: {document}"}],
)
return response.content[0].text`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Tool use</h2>
<p className="text-neutral-400 leading-relaxed mb-4">
When Claude invokes tools, AgentLens captures each tool use as a
TOOL_SELECTION decision point automatically:
</p>
<CodeBlock title="tools.py" language="python">{`@trace(name="claude-tool-agent")
async def tool_agent(prompt: str):
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=[{
"name": "get_stock_price",
"description": "Get the current stock price for a ticker symbol",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Stock ticker symbol"
}
},
"required": ["ticker"]
}
}],
messages=[{"role": "user", "content": prompt}],
)
return response`}</CodeBlock>
</section>
<section>
<h2 className="text-2xl font-semibold mb-4">Supported API methods</h2>
<ul className="text-sm text-neutral-400 space-y-2 ml-1">
<li>
<code className="font-mono text-emerald-400 text-xs">messages.create()</code> Message creation (including streaming)
</li>
<li>
<code className="font-mono text-emerald-400 text-xs">messages.count_tokens()</code> Token counting
</li>
</ul>
</section>
</div>
);
}

View File

@@ -0,0 +1,199 @@
import type { Metadata } from "next";
import { CodeBlock } from "@/components/code-block";
export const metadata: Metadata = {
title: "LangChain Integration",
description:
"Use the AgentLensCallbackHandler to trace LangChain chains, agents, and tool invocations.",
};
export default function LangChainIntegrationPage() {
return (
<div>
<h1 className="text-4xl font-bold tracking-tight mb-4">
LangChain Integration
</h1>
<p className="text-lg text-neutral-400 mb-10 leading-relaxed">
The AgentLensCallbackHandler plugs into LangChain&apos;s callback system
to automatically trace chains, agents, LLM calls, and tool invocations
without changing your existing code.
</p>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Installation</h2>
<CodeBlock title="terminal" language="bash">{`pip install vectry-agentlens langchain langchain-openai`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Quick setup</h2>
<CodeBlock title="main.py" language="python">{`import agentlens
from agentlens.integrations.langchain import AgentLensCallbackHandler
agentlens.init(
api_key="your-api-key",
endpoint="https://agentlens.vectry.tech",
)
handler = AgentLensCallbackHandler()`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Using with chains</h2>
<p className="text-neutral-400 leading-relaxed mb-4">
Pass the handler in the <code className="text-emerald-400 font-mono text-xs bg-emerald-500/5 px-1.5 py-0.5 rounded">callbacks</code> config:
</p>
<CodeBlock title="chain_example.py" language="python">{`from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
llm = ChatOpenAI(model="gpt-4o")
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant."),
("user", "{input}"),
])
chain = prompt | llm | StrOutputParser()
result = chain.invoke(
{"input": "Explain recursion"},
config={"callbacks": [handler]},
)`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Using with agents</h2>
<CodeBlock title="agent_example.py" language="python">{`from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import tool
@tool
def calculator(expression: str) -> str:
"""Evaluate a math expression."""
return str(eval(expression))
llm = ChatOpenAI(model="gpt-4o")
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful math assistant."),
("user", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
agent = create_tool_calling_agent(llm, [calculator], prompt)
executor = AgentExecutor(agent=agent, tools=[calculator])
result = executor.invoke(
{"input": "What is 42 * 17 + 3?"},
config={"callbacks": [handler]},
)`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">What gets captured</h2>
<p className="text-neutral-400 leading-relaxed mb-4">
The callback handler maps LangChain events to AgentLens concepts:
</p>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">LangChain Event</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">AgentLens Type</th>
<th className="text-left py-2 text-neutral-400 font-medium">Captured Data</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 text-neutral-400">Chain start/end</td>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">CHAIN span</td>
<td className="py-2">Input/output, duration</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 text-neutral-400">LLM start/end</td>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">LLM_CALL span</td>
<td className="py-2">Model, messages, tokens, cost, duration</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 text-neutral-400">Tool start/end</td>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">TOOL_CALL span</td>
<td className="py-2">Tool name, input args, output, duration</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 text-neutral-400">Agent action</td>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">TOOL_SELECTION decision</td>
<td className="py-2">Selected tool, reasoning</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 text-neutral-400">Retry</td>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">RETRY event</td>
<td className="py-2">Error message, attempt count</td>
</tr>
<tr>
<td className="py-2 pr-4 text-neutral-400">Error</td>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">ERROR event</td>
<td className="py-2">Exception type, message, traceback</td>
</tr>
</tbody>
</table>
</div>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Global callbacks</h2>
<p className="text-neutral-400 leading-relaxed mb-4">
To trace all LangChain operations without passing callbacks
individually, set the handler globally:
</p>
<CodeBlock title="global.py" language="python">{`from langchain_core.globals import set_llm_cache
from langchain.callbacks.manager import set_handler
set_handler(handler)
# Now all chains and agents are traced automatically
result = chain.invoke({"input": "Hello"})
# No need to pass config={"callbacks": [handler]}`}</CodeBlock>
</section>
<section>
<h2 className="text-2xl font-semibold mb-4">Handler options</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Parameter</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Type</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Default</th>
<th className="text-left py-2 text-neutral-400 font-medium">Description</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">trace_name</td>
<td className="py-2 pr-4 text-neutral-500">str | None</td>
<td className="py-2 pr-4 text-neutral-500">None</td>
<td className="py-2">Override the default trace name</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">tags</td>
<td className="py-2 pr-4 text-neutral-500">list[str]</td>
<td className="py-2 pr-4 text-neutral-500">[]</td>
<td className="py-2">Tags to attach to all traces</td>
</tr>
<tr>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">capture_io</td>
<td className="py-2 pr-4 text-neutral-500">bool</td>
<td className="py-2 pr-4 text-neutral-500">True</td>
<td className="py-2">Capture input/output payloads</td>
</tr>
</tbody>
</table>
</div>
<CodeBlock title="options.py" language="python">{`handler = AgentLensCallbackHandler(
trace_name="my-langchain-app",
tags=["production", "langchain"],
capture_io=True,
)`}</CodeBlock>
</section>
</div>
);
}

View File

@@ -0,0 +1,188 @@
import type { Metadata } from "next";
import { CodeBlock } from "@/components/code-block";
export const metadata: Metadata = {
title: "OpenAI Integration",
description:
"Auto-trace all OpenAI API calls with a single wrapper. Captures model, tokens, cost, and latency.",
};
export default function OpenAIIntegrationPage() {
return (
<div>
<h1 className="text-4xl font-bold tracking-tight mb-4">
OpenAI Integration
</h1>
<p className="text-lg text-neutral-400 mb-10 leading-relaxed">
Wrap the OpenAI client once and every API call is automatically traced.
AgentLens captures the model name, token usage, cost, latency, input
messages, and output completions.
</p>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Installation</h2>
<CodeBlock title="terminal" language="bash">{`pip install vectry-agentlens openai`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Quick setup</h2>
<CodeBlock title="main.py" language="python">{`import agentlens
from agentlens.integrations.openai import wrap_openai
import openai
agentlens.init(
api_key="your-api-key",
endpoint="https://agentlens.vectry.tech",
)
client = wrap_openai(openai.OpenAI())
# All calls are now auto-traced
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain quantum computing in one paragraph."},
],
)`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">What gets captured</h2>
<p className="text-neutral-400 leading-relaxed mb-4">
Each OpenAI API call creates an LLM_CALL span with the following data:
</p>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Field</th>
<th className="text-left py-2 text-neutral-400 font-medium">Description</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">input.model</td>
<td className="py-2">Model name (gpt-4o, gpt-4o-mini, etc.)</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">input.messages</td>
<td className="py-2">Full message array sent to the API</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">output.content</td>
<td className="py-2">Response content from the model</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">tokenCount</td>
<td className="py-2">Total tokens (prompt + completion)</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">costUsd</td>
<td className="py-2">Estimated cost based on model pricing</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">durationMs</td>
<td className="py-2">Wall-clock time for the request</td>
</tr>
<tr>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">metadata.finish_reason</td>
<td className="py-2">How the model stopped (stop, length, tool_calls)</td>
</tr>
</tbody>
</table>
</div>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Async client</h2>
<p className="text-neutral-400 leading-relaxed mb-4">
The wrapper works with both sync and async OpenAI clients:
</p>
<CodeBlock title="async_example.py" language="python">{`from agentlens.integrations.openai import wrap_openai
import openai
async_client = wrap_openai(openai.AsyncOpenAI())
response = await async_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello!"}],
)`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">
Combining with @trace
</h2>
<p className="text-neutral-400 leading-relaxed mb-4">
When used inside a <code className="text-emerald-400 font-mono text-xs bg-emerald-500/5 px-1.5 py-0.5 rounded">@trace</code>-decorated
function, OpenAI calls appear as child spans of the trace:
</p>
<CodeBlock title="combined.py" language="python">{`import agentlens
from agentlens import trace
from agentlens.integrations.openai import wrap_openai
import openai
agentlens.init(api_key="...", endpoint="...")
client = wrap_openai(openai.OpenAI())
@trace(name="research-agent")
async def research(topic: str) -> str:
# This LLM call becomes a child span of "research-agent"
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "Summarize the following topic."},
{"role": "user", "content": topic},
],
)
return response.choices[0].message.content`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Tool calls</h2>
<p className="text-neutral-400 leading-relaxed mb-4">
When the model invokes tools (function calling), AgentLens
automatically captures each tool call as a TOOL_SELECTION decision
point and the tool execution as a TOOL_CALL span:
</p>
<CodeBlock title="tools.py" language="python">{`@trace(name="tool-agent")
async def agent_with_tools(prompt: str):
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
tools=[{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
},
},
}],
)
# AgentLens captures the tool selection decision automatically
return response`}</CodeBlock>
</section>
<section>
<h2 className="text-2xl font-semibold mb-4">Supported API methods</h2>
<ul className="text-sm text-neutral-400 space-y-2 ml-1">
<li>
<code className="font-mono text-emerald-400 text-xs">chat.completions.create()</code> Chat completions (including streaming)
</li>
<li>
<code className="font-mono text-emerald-400 text-xs">completions.create()</code> Legacy completions
</li>
<li>
<code className="font-mono text-emerald-400 text-xs">embeddings.create()</code> Embedding generation
</li>
</ul>
</section>
</div>
);
}

View File

@@ -0,0 +1,63 @@
import type { Metadata } from "next";
import { DocsSidebar } from "./docs-sidebar";
export const metadata: Metadata = {
title: {
default: "Documentation",
template: "%s | AgentLens Docs",
},
description:
"AgentLens documentation — learn how to instrument, trace, and observe your AI agents.",
};
export default function DocsLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<div className="min-h-screen bg-neutral-950">
<header className="sticky top-0 z-50 border-b border-neutral-800/50 bg-neutral-950/80 backdrop-blur-md">
<div className="max-w-[90rem] mx-auto flex items-center justify-between h-14 px-4 sm:px-6 lg:px-8">
<div className="flex items-center gap-6">
<a href="/" className="flex items-center gap-2 text-neutral-100 hover:text-emerald-400 transition-colors">
<div className="w-7 h-7 rounded bg-gradient-to-br from-emerald-400 to-emerald-600 flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<span className="font-semibold text-sm">AgentLens</span>
</a>
<span className="text-neutral-600">/</span>
<a href="/docs" className="text-sm text-neutral-400 hover:text-neutral-200 transition-colors">
Documentation
</a>
</div>
<div className="flex items-center gap-4">
<a
href="/dashboard"
className="text-sm text-neutral-400 hover:text-neutral-200 transition-colors"
>
Dashboard
</a>
<a
href="https://gitea.repi.fun/repi/agentlens"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-neutral-400 hover:text-neutral-200 transition-colors"
>
Source
</a>
</div>
</div>
</header>
<div className="max-w-[90rem] mx-auto flex">
<DocsSidebar />
<main className="flex-1 min-w-0 px-4 sm:px-8 lg:px-12 py-10 lg:py-12">
<div className="max-w-3xl">{children}</div>
</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,216 @@
import type { Metadata } from "next";
import { CodeBlock } from "@/components/code-block";
export const metadata: Metadata = {
title: "OpenCode Plugin",
description:
"Capture OpenCode sessions including tool calls, LLM calls, file edits, and git diffs with the AgentLens OpenCode plugin.",
};
export default function OpenCodePluginPage() {
return (
<div>
<h1 className="text-4xl font-bold tracking-tight mb-4">
OpenCode Plugin
</h1>
<p className="text-lg text-neutral-400 mb-10 leading-relaxed">
The AgentLens OpenCode plugin captures everything that happens during an
OpenCode coding session and sends it as structured traces to your
AgentLens instance.
</p>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Installation</h2>
<CodeBlock title="terminal" language="bash">{`npm install opencode-agentlens`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Configuration</h2>
<p className="text-neutral-400 leading-relaxed mb-4">
Add the plugin to your <code className="text-emerald-400 font-mono text-xs bg-emerald-500/5 px-1.5 py-0.5 rounded">opencode.json</code> configuration file:
</p>
<CodeBlock title="opencode.json" language="json">{`{
"plugin": ["opencode-agentlens"]
}`}</CodeBlock>
<p className="text-neutral-400 leading-relaxed mt-4 mb-4">
Set the required environment variables:
</p>
<CodeBlock title="terminal" language="bash">{`export AGENTLENS_API_KEY="your-api-key"
export AGENTLENS_ENDPOINT="https://agentlens.vectry.tech"`}</CodeBlock>
<p className="text-neutral-400 leading-relaxed mt-4">
You can also add these to a <code className="text-emerald-400 font-mono text-xs bg-emerald-500/5 px-1.5 py-0.5 rounded">.env</code> file in your project root.
</p>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">What gets captured</h2>
<p className="text-neutral-400 leading-relaxed mb-6">
Every OpenCode session becomes a trace with nested spans and events
for each action taken during the session:
</p>
<div className="space-y-3">
<div className="p-4 rounded-lg border border-neutral-800/50 bg-neutral-900/30">
<div className="flex items-center gap-3 mb-2">
<span className="px-2 py-0.5 rounded text-xs font-mono bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
AGENT span
</span>
<span className="text-sm font-medium text-neutral-200">
Sessions
</span>
</div>
<p className="text-sm text-neutral-500">
Each OpenCode session is captured as a top-level AGENT span.
Includes session ID, start time, end time, and overall status.
</p>
</div>
<div className="p-4 rounded-lg border border-neutral-800/50 bg-neutral-900/30">
<div className="flex items-center gap-3 mb-2">
<span className="px-2 py-0.5 rounded text-xs font-mono bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
LLM_CALL span
</span>
<span className="text-sm font-medium text-neutral-200">
LLM calls
</span>
</div>
<p className="text-sm text-neutral-500">
Every call to an LLM provider (Claude, GPT, etc.) is recorded with
the full prompt, response, token counts, and cost.
</p>
</div>
<div className="p-4 rounded-lg border border-neutral-800/50 bg-neutral-900/30">
<div className="flex items-center gap-3 mb-2">
<span className="px-2 py-0.5 rounded text-xs font-mono bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
TOOL_CALL span
</span>
<span className="text-sm font-medium text-neutral-200">
Tool calls
</span>
</div>
<p className="text-sm text-neutral-500">
Tool invocations including file reads, writes, shell commands,
search operations, and MCP tool calls. Captures input arguments
and outputs.
</p>
</div>
<div className="p-4 rounded-lg border border-neutral-800/50 bg-neutral-900/30">
<div className="flex items-center gap-3 mb-2">
<span className="px-2 py-0.5 rounded text-xs font-mono bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
TOOL_SELECTION decision
</span>
<span className="text-sm font-medium text-neutral-200">
Permissions
</span>
</div>
<p className="text-sm text-neutral-500">
Permission requests and grants are captured as decision points,
showing what the agent asked to do and whether it was allowed.
</p>
</div>
<div className="p-4 rounded-lg border border-neutral-800/50 bg-neutral-900/30">
<div className="flex items-center gap-3 mb-2">
<span className="px-2 py-0.5 rounded text-xs font-mono bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
CUSTOM span
</span>
<span className="text-sm font-medium text-neutral-200">
File edits
</span>
</div>
<p className="text-sm text-neutral-500">
Every file creation, modification, and deletion is tracked with
before/after content diffs.
</p>
</div>
<div className="p-4 rounded-lg border border-neutral-800/50 bg-neutral-900/30">
<div className="flex items-center gap-3 mb-2">
<span className="px-2 py-0.5 rounded text-xs font-mono bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
CUSTOM event
</span>
<span className="text-sm font-medium text-neutral-200">
Git diffs
</span>
</div>
<p className="text-sm text-neutral-500">
Git operations (commits, diffs, branch changes) are captured as
events with the full diff content.
</p>
</div>
</div>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Trace structure</h2>
<p className="text-neutral-400 leading-relaxed mb-4">
A typical OpenCode session trace looks like this:
</p>
<CodeBlock>{`Trace: "opencode-session-abc123"
|
+-- Span: "session" (AGENT)
| +-- Span: "read-file: src/main.ts" (TOOL_CALL)
| +-- Span: "llm-call: claude-sonnet" (LLM_CALL)
| | Decision: TOOL_SELECTION -> chose "edit-file" over "write-file"
| +-- Span: "edit-file: src/main.ts" (TOOL_CALL)
| +-- Span: "llm-call: claude-sonnet" (LLM_CALL)
| +-- Span: "bash: npm test" (TOOL_CALL)
| +-- Event: "git-diff" (CUSTOM)
| +-- Span: "bash: git commit" (TOOL_CALL)`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Environment variables</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Variable</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Required</th>
<th className="text-left py-2 text-neutral-400 font-medium">Description</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">AGENTLENS_API_KEY</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">API key for authentication</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">AGENTLENS_ENDPOINT</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2">AgentLens server URL</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">AGENTLENS_ENABLED</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Set to &quot;false&quot; to disable (default: &quot;true&quot;)</td>
</tr>
<tr>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">AGENTLENS_SESSION_TAGS</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2">Comma-separated tags to add to all session traces</td>
</tr>
</tbody>
</table>
</div>
</section>
<section>
<h2 className="text-2xl font-semibold mb-4">Filtering sensitive data</h2>
<p className="text-neutral-400 leading-relaxed mb-4">
By default, the plugin captures full file contents and command outputs.
To filter sensitive data, set the <code className="text-emerald-400 font-mono text-xs bg-emerald-500/5 px-1.5 py-0.5 rounded">AGENTLENS_REDACT_PATTERNS</code> environment variable with a comma-separated list of regex patterns:
</p>
<CodeBlock title="terminal" language="bash">{`export AGENTLENS_REDACT_PATTERNS="password=.*,API_KEY=.*,Bearer .*"`}</CodeBlock>
<p className="text-neutral-400 leading-relaxed mt-4">
Matched content is replaced with <code className="text-emerald-400 font-mono text-xs bg-emerald-500/5 px-1.5 py-0.5 rounded">[REDACTED]</code> before
being sent to the server.
</p>
</section>
</div>
);
}

View File

@@ -0,0 +1,131 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Documentation",
description:
"AgentLens documentation — instrument, trace, and observe your AI agents with full decision visibility.",
};
const sections = [
{
heading: "Getting Started",
items: [
{
title: "Quick Start",
href: "/docs/getting-started",
description:
"Install the SDK, initialize AgentLens, and send your first trace in under five minutes.",
},
{
title: "Core Concepts",
href: "/docs/concepts",
description:
"Understand Traces, Spans, Decision Points, and Events — the four building blocks of AgentLens.",
},
],
},
{
heading: "SDKs",
items: [
{
title: "Python SDK",
href: "/docs/python-sdk",
description:
"Full reference for the Python SDK: init(), @trace decorator, log_decision(), TraceContext, and configuration.",
},
{
title: "TypeScript SDK",
href: "/docs/typescript-sdk",
description:
"Full reference for the TypeScript SDK: init(), TraceBuilder API, createDecision(), and shutdown().",
},
],
},
{
heading: "Integrations",
items: [
{
title: "OpenAI",
href: "/docs/integrations/openai",
description:
"Auto-trace all OpenAI API calls with a single wrapper. Captures model, tokens, cost, and latency.",
},
{
title: "Anthropic",
href: "/docs/integrations/anthropic",
description:
"Wrap the Anthropic client to automatically trace Claude API calls with full metadata capture.",
},
{
title: "LangChain",
href: "/docs/integrations/langchain",
description:
"Use the AgentLensCallbackHandler to trace LangChain chains, agents, and tool invocations.",
},
],
},
{
heading: "Tools & Deployment",
items: [
{
title: "OpenCode Plugin",
href: "/docs/opencode-plugin",
description:
"Capture OpenCode sessions including tool calls, LLM calls, file edits, and git diffs automatically.",
},
{
title: "REST API Reference",
href: "/docs/api-reference",
description:
"Complete contract for POST /api/traces and GET /api/traces including payload shapes and error codes.",
},
{
title: "Self-Hosting",
href: "/docs/self-hosting",
description:
"Deploy AgentLens with Docker or from source. Configure database, API keys, and environment variables.",
},
],
},
];
export default function DocsPage() {
return (
<div>
<h1 className="text-4xl font-bold tracking-tight mb-4">
AgentLens Documentation
</h1>
<p className="text-lg text-neutral-400 mb-12 leading-relaxed max-w-2xl">
AgentLens is an open-source agent observability platform that traces
decisions, not just API calls. These docs cover everything from initial
setup to advanced self-hosting.
</p>
<div className="space-y-12">
{sections.map((section) => (
<div key={section.heading}>
<h2 className="text-xs font-semibold uppercase tracking-wider text-neutral-500 mb-4">
{section.heading}
</h2>
<div className="grid sm:grid-cols-2 gap-4">
{section.items.map((item) => (
<a
key={item.href}
href={item.href}
className="group block p-5 rounded-xl border border-neutral-800/50 bg-neutral-900/30 hover:border-emerald-500/30 hover:bg-neutral-900/60 transition-all duration-200"
>
<h3 className="text-base font-semibold text-neutral-100 group-hover:text-emerald-400 transition-colors mb-1.5">
{item.title}
</h3>
<p className="text-sm text-neutral-500 leading-relaxed">
{item.description}
</p>
</a>
))}
</div>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,313 @@
import type { Metadata } from "next";
import { CodeBlock } from "@/components/code-block";
export const metadata: Metadata = {
title: "Python SDK",
description:
"Full reference for the AgentLens Python SDK: init(), @trace decorator, log_decision(), TraceContext, and configuration.",
};
function ApiSection({
name,
signature,
description,
children,
}: {
name: string;
signature: string;
description: string;
children?: React.ReactNode;
}) {
return (
<div className="mb-10 pb-10 border-b border-neutral-800/50 last:border-0">
<h3 className="text-xl font-semibold mb-1">{name}</h3>
<div className="px-3 py-1.5 rounded-lg bg-neutral-900/50 border border-neutral-800/50 inline-block mb-3">
<code className="text-sm font-mono text-emerald-400">{signature}</code>
</div>
<p className="text-neutral-400 leading-relaxed mb-4">{description}</p>
{children}
</div>
);
}
export default function PythonSdkPage() {
return (
<div>
<h1 className="text-4xl font-bold tracking-tight mb-4">Python SDK</h1>
<p className="text-lg text-neutral-400 mb-4 leading-relaxed">
The AgentLens Python SDK provides decorators, context managers, and
helper functions to instrument your AI agents.
</p>
<div className="px-4 py-3 rounded-lg bg-neutral-900/50 border border-neutral-800/50 mb-10">
<code className="text-sm font-mono text-emerald-400">
pip install vectry-agentlens
</code>
</div>
<h2 className="text-2xl font-semibold mb-6">API Reference</h2>
<ApiSection
name="init()"
signature="agentlens.init(api_key, endpoint, *, flush_interval=5.0, max_batch_size=100, enabled=True)"
description="Initialize the AgentLens SDK. Must be called before any tracing functions. Typically called once at application startup."
>
<h4 className="text-sm font-medium text-neutral-300 mb-2">
Parameters
</h4>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Parameter</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Type</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Default</th>
<th className="text-left py-2 text-neutral-400 font-medium">Description</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">api_key</td>
<td className="py-2 pr-4 text-neutral-500">str</td>
<td className="py-2 pr-4 text-neutral-500">required</td>
<td className="py-2">Your AgentLens API key</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">endpoint</td>
<td className="py-2 pr-4 text-neutral-500">str</td>
<td className="py-2 pr-4 text-neutral-500">required</td>
<td className="py-2">AgentLens server URL</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">flush_interval</td>
<td className="py-2 pr-4 text-neutral-500">float</td>
<td className="py-2 pr-4 text-neutral-500">5.0</td>
<td className="py-2">Seconds between automatic flushes</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">max_batch_size</td>
<td className="py-2 pr-4 text-neutral-500">int</td>
<td className="py-2 pr-4 text-neutral-500">100</td>
<td className="py-2">Max traces per batch request</td>
</tr>
<tr>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">enabled</td>
<td className="py-2 pr-4 text-neutral-500">bool</td>
<td className="py-2 pr-4 text-neutral-500">True</td>
<td className="py-2">Set to False to disable tracing globally</td>
</tr>
</tbody>
</table>
</div>
<CodeBlock title="example.py" language="python">{`import agentlens
agentlens.init(
api_key="al_key_abc123",
endpoint="https://agentlens.vectry.tech",
flush_interval=10.0,
max_batch_size=50,
)`}</CodeBlock>
</ApiSection>
<ApiSection
name="@trace"
signature='@agentlens.trace(name=None, tags=None, metadata=None)'
description="Decorator that wraps a function in a trace. The trace starts when the function is called and ends when it returns or raises. Works with both sync and async functions."
>
<h4 className="text-sm font-medium text-neutral-300 mb-2">
Parameters
</h4>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Parameter</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Type</th>
<th className="text-left py-2 text-neutral-400 font-medium">Description</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">name</td>
<td className="py-2 pr-4 text-neutral-500">str | None</td>
<td className="py-2">Trace name. Defaults to the function name.</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">tags</td>
<td className="py-2 pr-4 text-neutral-500">list[str] | None</td>
<td className="py-2">Tags to attach to the trace</td>
</tr>
<tr>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">metadata</td>
<td className="py-2 pr-4 text-neutral-500">dict | None</td>
<td className="py-2">Arbitrary metadata dict</td>
</tr>
</tbody>
</table>
</div>
<CodeBlock title="decorator.py" language="python">{`from agentlens import trace
@trace(name="research-agent", tags=["research", "v2"])
async def research(topic: str) -> str:
result = await search(topic)
summary = await summarize(result)
return summary
# Can also be used without arguments
@trace
def simple_agent(prompt: str) -> str:
return call_llm(prompt)`}</CodeBlock>
</ApiSection>
<ApiSection
name="log_decision()"
signature="agentlens.log_decision(type, chosen, alternatives, *, reasoning=None, context_snapshot=None)"
description="Log a decision point within the current trace context. Must be called from within a @trace-decorated function."
>
<h4 className="text-sm font-medium text-neutral-300 mb-2">
Parameters
</h4>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Parameter</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Type</th>
<th className="text-left py-2 text-neutral-400 font-medium">Description</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">type</td>
<td className="py-2 pr-4 text-neutral-500">str</td>
<td className="py-2">One of: TOOL_SELECTION, ROUTING, RETRY, ESCALATION, MEMORY_RETRIEVAL, PLANNING, CUSTOM</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">chosen</td>
<td className="py-2 pr-4 text-neutral-500">dict</td>
<td className="py-2">What was chosen</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">alternatives</td>
<td className="py-2 pr-4 text-neutral-500">list[dict]</td>
<td className="py-2">What else was considered</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">reasoning</td>
<td className="py-2 pr-4 text-neutral-500">str | None</td>
<td className="py-2">Why this choice was made</td>
</tr>
<tr>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">context_snapshot</td>
<td className="py-2 pr-4 text-neutral-500">dict | None</td>
<td className="py-2">Snapshot of context at decision time</td>
</tr>
</tbody>
</table>
</div>
<CodeBlock title="decisions.py" language="python">{`import agentlens
from agentlens import trace
@trace(name="routing-agent")
async def route_request(user_input: str):
intent = classify_intent(user_input)
agentlens.log_decision(
type="ROUTING",
chosen={"handler": "refund", "confidence": 0.92},
alternatives=[
{"handler": "faq", "confidence": 0.65},
{"handler": "escalate", "confidence": 0.23},
],
reasoning="High confidence refund intent detected",
context_snapshot={"intent": intent, "input_length": len(user_input)},
)
return await handle_refund(user_input)`}</CodeBlock>
</ApiSection>
<ApiSection
name="TraceContext"
signature="agentlens.TraceContext"
description="Context manager for manual trace lifecycle control. Use this when the @trace decorator does not fit your workflow."
>
<CodeBlock title="context.py" language="python">{`import agentlens
async def process_batch(items: list[str]):
for item in items:
ctx = agentlens.TraceContext(
name=f"process-{item}",
tags=["batch"],
)
ctx.start()
try:
result = await process(item)
ctx.add_span(
name="process",
type="CUSTOM",
input={"item": item},
output={"result": result},
status="COMPLETED",
)
ctx.end(status="COMPLETED")
except Exception as e:
ctx.add_event(type="ERROR", name=str(e))
ctx.end(status="ERROR")`}</CodeBlock>
</ApiSection>
<ApiSection
name="shutdown()"
signature="agentlens.shutdown(timeout=10.0)"
description="Flush all pending traces and shut down the background sender. Call this before your application exits to avoid losing data."
>
<CodeBlock title="shutdown.py" language="python">{`import agentlens
import atexit
agentlens.init(api_key="...", endpoint="...")
# Register shutdown hook
atexit.register(agentlens.shutdown)
# Or call manually
agentlens.shutdown(timeout=30.0)`}</CodeBlock>
</ApiSection>
<section className="mt-12">
<h2 className="text-2xl font-semibold mb-4">Configuration</h2>
<p className="text-neutral-400 leading-relaxed mb-4">
The SDK can also be configured via environment variables. These take
precedence over values passed to <code className="text-emerald-400 font-mono text-xs bg-emerald-500/5 px-1.5 py-0.5 rounded">init()</code>.
</p>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Variable</th>
<th className="text-left py-2 text-neutral-400 font-medium">Description</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">AGENTLENS_API_KEY</td>
<td className="py-2">API key for authentication</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">AGENTLENS_ENDPOINT</td>
<td className="py-2">Server URL</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">AGENTLENS_ENABLED</td>
<td className="py-2">Set to &quot;false&quot; to disable tracing</td>
</tr>
<tr>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">AGENTLENS_FLUSH_INTERVAL</td>
<td className="py-2">Flush interval in seconds</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
);
}

View File

@@ -0,0 +1,241 @@
import type { Metadata } from "next";
import { CodeBlock } from "@/components/code-block";
export const metadata: Metadata = {
title: "Self-Hosting",
description:
"Deploy AgentLens with Docker or from source. Configure database, API keys, and environment variables.",
};
export default function SelfHostingPage() {
return (
<div>
<h1 className="text-4xl font-bold tracking-tight mb-4">Self-Hosting</h1>
<p className="text-lg text-neutral-400 mb-10 leading-relaxed">
AgentLens is open source and designed to be self-hosted. You can deploy
it with Docker in minutes, or run from source for development.
</p>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Quick start with Docker</h2>
<CodeBlock title="terminal" language="bash">{`git clone https://gitea.repi.fun/repi/agentlens
cd agentlens
docker build -t agentlens .
docker run -p 3000:3000 \\
-e DATABASE_URL="postgresql://user:pass@host:5432/agentlens" \\
-e AGENTLENS_API_KEY="your-secret-key" \\
agentlens`}</CodeBlock>
<p className="text-neutral-400 leading-relaxed mt-4">
The dashboard will be available at{" "}
<code className="text-emerald-400 font-mono text-xs bg-emerald-500/5 px-1.5 py-0.5 rounded">
http://localhost:3000
</code>{" "}
and the API at{" "}
<code className="text-emerald-400 font-mono text-xs bg-emerald-500/5 px-1.5 py-0.5 rounded">
http://localhost:3000/api/traces
</code>
.
</p>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Docker Compose</h2>
<p className="text-neutral-400 leading-relaxed mb-4">
For a complete setup with PostgreSQL included:
</p>
<CodeBlock title="docker-compose.yml" language="yaml">{`version: "3.8"
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: agentlens
POSTGRES_PASSWORD: agentlens
POSTGRES_DB: agentlens
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: "postgresql://agentlens:agentlens@db:5432/agentlens"
AGENTLENS_API_KEY: "your-secret-key"
PORT: "3000"
depends_on:
- db
volumes:
pgdata:`}</CodeBlock>
<CodeBlock title="terminal" language="bash">{`docker compose up -d`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Running from source</h2>
<p className="text-neutral-400 leading-relaxed mb-4">
For development or when you need to customize AgentLens:
</p>
<CodeBlock title="terminal" language="bash">{`git clone https://gitea.repi.fun/repi/agentlens
cd agentlens
# Install dependencies (uses npm workspaces)
npm install
# Set up the database
cp apps/web/.env.example apps/web/.env
# Edit .env with your DATABASE_URL
# Generate Prisma client and push schema
npm run db:generate --workspace=@agentlens/web
npm run db:push --workspace=@agentlens/web
# Start the development server
npm run dev --workspace=@agentlens/web`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Environment variables</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Variable</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Required</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Default</th>
<th className="text-left py-2 text-neutral-400 font-medium">Description</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">DATABASE_URL</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2 pr-4 text-neutral-500">-</td>
<td className="py-2">PostgreSQL connection string</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">AGENTLENS_API_KEY</td>
<td className="py-2 pr-4 text-neutral-500">Yes</td>
<td className="py-2 pr-4 text-neutral-500">-</td>
<td className="py-2">API key that SDKs must present to ingest traces</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">PORT</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2 pr-4 text-neutral-500">3000</td>
<td className="py-2">HTTP port the server listens on</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">NODE_ENV</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2 pr-4 text-neutral-500">production</td>
<td className="py-2">Set to &quot;development&quot; for dev mode</td>
</tr>
<tr>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">NEXTAUTH_SECRET</td>
<td className="py-2 pr-4 text-neutral-500">No</td>
<td className="py-2 pr-4 text-neutral-500">-</td>
<td className="py-2">Secret for session signing (if auth is enabled)</td>
</tr>
</tbody>
</table>
</div>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Database setup</h2>
<p className="text-neutral-400 leading-relaxed mb-4">
AgentLens uses PostgreSQL with Prisma ORM. The database schema is
managed via Prisma migrations.
</p>
<h3 className="text-lg font-medium text-neutral-200 mb-2">
Connection string format
</h3>
<CodeBlock>{`postgresql://USER:PASSWORD@HOST:PORT/DATABASE`}</CodeBlock>
<h3 className="text-lg font-medium text-neutral-200 mb-2 mt-6">
Running migrations
</h3>
<CodeBlock title="terminal" language="bash">{`# Push schema to database (development)
npm run db:push --workspace=@agentlens/web
# Run migrations (production)
npm run db:migrate --workspace=@agentlens/web`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Reverse proxy setup</h2>
<p className="text-neutral-400 leading-relaxed mb-4">
For production deployments behind nginx or Caddy:
</p>
<CodeBlock title="Caddyfile" language="bash">{`agentlens.yourdomain.com {
reverse_proxy localhost:3000
}`}</CodeBlock>
<CodeBlock title="nginx.conf" language="bash">{`server {
listen 443 ssl;
server_name agentlens.yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}`}</CodeBlock>
</section>
<section className="mb-12">
<h2 className="text-2xl font-semibold mb-4">Updating</h2>
<CodeBlock title="terminal" language="bash">{`# Pull latest changes
cd agentlens
git pull origin main
# Rebuild
docker build -t agentlens .
# Restart with new image
docker compose up -d`}</CodeBlock>
</section>
<section>
<h2 className="text-2xl font-semibold mb-4">Resource requirements</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Component</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Minimum</th>
<th className="text-left py-2 text-neutral-400 font-medium">Recommended</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 text-neutral-400">CPU</td>
<td className="py-2 pr-4">1 core</td>
<td className="py-2">2+ cores</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 text-neutral-400">Memory</td>
<td className="py-2 pr-4">512 MB</td>
<td className="py-2">1 GB+</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 text-neutral-400">Disk</td>
<td className="py-2 pr-4">1 GB</td>
<td className="py-2">10 GB+ (depends on trace volume)</td>
</tr>
<tr>
<td className="py-2 pr-4 text-neutral-400">PostgreSQL</td>
<td className="py-2 pr-4">14+</td>
<td className="py-2">16+</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
);
}

View File

@@ -0,0 +1,287 @@
import type { Metadata } from "next";
import { CodeBlock } from "@/components/code-block";
export const metadata: Metadata = {
title: "TypeScript SDK",
description:
"Full reference for the AgentLens TypeScript SDK: init(), TraceBuilder, createDecision(), and shutdown().",
};
function ApiSection({
name,
signature,
description,
children,
}: {
name: string;
signature: string;
description: string;
children?: React.ReactNode;
}) {
return (
<div className="mb-10 pb-10 border-b border-neutral-800/50 last:border-0">
<h3 className="text-xl font-semibold mb-1">{name}</h3>
<div className="px-3 py-1.5 rounded-lg bg-neutral-900/50 border border-neutral-800/50 inline-block mb-3">
<code className="text-sm font-mono text-emerald-400">{signature}</code>
</div>
<p className="text-neutral-400 leading-relaxed mb-4">{description}</p>
{children}
</div>
);
}
export default function TypeScriptSdkPage() {
return (
<div>
<h1 className="text-4xl font-bold tracking-tight mb-4">
TypeScript SDK
</h1>
<p className="text-lg text-neutral-400 mb-4 leading-relaxed">
The AgentLens TypeScript SDK provides a builder-based API for
constructing and sending traces from Node.js and edge runtimes.
</p>
<div className="px-4 py-3 rounded-lg bg-neutral-900/50 border border-neutral-800/50 mb-10">
<code className="text-sm font-mono text-emerald-400">
npm install agentlens-sdk
</code>
</div>
<h2 className="text-2xl font-semibold mb-6">API Reference</h2>
<ApiSection
name="init()"
signature='init({ apiKey, endpoint, flushInterval?, maxBatchSize?, enabled? })'
description="Initialize the SDK. Must be called once before creating any traces."
>
<h4 className="text-sm font-medium text-neutral-300 mb-2">
Options
</h4>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Property</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Type</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Default</th>
<th className="text-left py-2 text-neutral-400 font-medium">Description</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">apiKey</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">required</td>
<td className="py-2">Your AgentLens API key</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">endpoint</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2 pr-4 text-neutral-500">required</td>
<td className="py-2">AgentLens server URL</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">flushInterval</td>
<td className="py-2 pr-4 text-neutral-500">number</td>
<td className="py-2 pr-4 text-neutral-500">5000</td>
<td className="py-2">Milliseconds between flushes</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">maxBatchSize</td>
<td className="py-2 pr-4 text-neutral-500">number</td>
<td className="py-2 pr-4 text-neutral-500">100</td>
<td className="py-2">Max traces per batch</td>
</tr>
<tr>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">enabled</td>
<td className="py-2 pr-4 text-neutral-500">boolean</td>
<td className="py-2 pr-4 text-neutral-500">true</td>
<td className="py-2">Toggle tracing on/off</td>
</tr>
</tbody>
</table>
</div>
<CodeBlock title="init.ts" language="typescript">{`import { init } from "agentlens-sdk";
init({
apiKey: process.env.AGENTLENS_API_KEY!,
endpoint: "https://agentlens.vectry.tech",
flushInterval: 10000,
});`}</CodeBlock>
</ApiSection>
<ApiSection
name="TraceBuilder"
signature='new TraceBuilder(name, options?)'
description="Builder for constructing a trace incrementally. Add spans, decision points, and events, then call end() to finalize and send."
>
<h4 className="text-sm font-medium text-neutral-300 mb-2">
Constructor options
</h4>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Property</th>
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Type</th>
<th className="text-left py-2 text-neutral-400 font-medium">Description</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">tags</td>
<td className="py-2 pr-4 text-neutral-500">string[]</td>
<td className="py-2">Tags for this trace</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">sessionId</td>
<td className="py-2 pr-4 text-neutral-500">string</td>
<td className="py-2">Group traces into a session</td>
</tr>
<tr>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">metadata</td>
<td className="py-2 pr-4 text-neutral-500">Record&lt;string, unknown&gt;</td>
<td className="py-2">Arbitrary metadata</td>
</tr>
</tbody>
</table>
</div>
<h4 className="text-sm font-medium text-neutral-300 mb-2 mt-6">
Methods
</h4>
<div className="space-y-4 mb-6">
<div className="p-3 rounded-lg border border-neutral-800/50 bg-neutral-900/30">
<code className="text-sm font-mono text-emerald-400">
addSpan(span: SpanInput): string
</code>
<p className="text-xs text-neutral-500 mt-1">
Add a span to the trace. Returns the generated span ID. Pass <code className="text-emerald-400/80 font-mono bg-emerald-500/5 px-1 rounded">parentSpanId</code> to nest spans.
</p>
</div>
<div className="p-3 rounded-lg border border-neutral-800/50 bg-neutral-900/30">
<code className="text-sm font-mono text-emerald-400">
addDecision(decision: DecisionInput): string
</code>
<p className="text-xs text-neutral-500 mt-1">
Add a decision point. Returns the generated decision ID.
</p>
</div>
<div className="p-3 rounded-lg border border-neutral-800/50 bg-neutral-900/30">
<code className="text-sm font-mono text-emerald-400">
addEvent(event: EventInput): string
</code>
<p className="text-xs text-neutral-500 mt-1">
Add an event to the trace. Returns the generated event ID.
</p>
</div>
<div className="p-3 rounded-lg border border-neutral-800/50 bg-neutral-900/30">
<code className="text-sm font-mono text-emerald-400">
end(status?: &quot;COMPLETED&quot; | &quot;ERROR&quot;): Promise&lt;void&gt;
</code>
<p className="text-xs text-neutral-500 mt-1">
Finalize and send the trace. Defaults to COMPLETED.
</p>
</div>
</div>
<CodeBlock title="trace-builder.ts" language="typescript">{`import { TraceBuilder } from "agentlens-sdk";
const trace = new TraceBuilder("customer-support", {
tags: ["support", "v2"],
sessionId: "session-abc",
});
const agentSpan = trace.addSpan({
name: "classify-intent",
type: "LLM_CALL",
input: { messages: [{ role: "user", content: "I need a refund" }] },
output: { intent: "refund", confidence: 0.95 },
status: "COMPLETED",
tokenCount: 150,
costUsd: 0.002,
durationMs: 340,
});
trace.addDecision({
type: "ROUTING",
chosen: { handler: "refund-flow" },
alternatives: [{ handler: "faq-flow" }, { handler: "escalate" }],
reasoning: "High confidence refund intent",
parentSpanId: agentSpan,
});
trace.addSpan({
name: "process-refund",
type: "TOOL_CALL",
input: { orderId: "ord-123" },
output: { success: true },
status: "COMPLETED",
parentSpanId: agentSpan,
});
await trace.end();`}</CodeBlock>
</ApiSection>
<ApiSection
name="createDecision()"
signature='createDecision(type, chosen, alternatives, options?)'
description="Standalone helper to create a decision point outside of a TraceBuilder. Useful when building traces from raw data."
>
<CodeBlock title="standalone.ts" language="typescript">{`import { createDecision } from "agentlens-sdk";
const decision = createDecision(
"TOOL_SELECTION",
{ tool: "calculator", confidence: 0.88 },
[
{ tool: "web_search", confidence: 0.52 },
{ tool: "code_exec", confidence: 0.34 },
],
{ reasoning: "Math expression detected in input" }
);`}</CodeBlock>
</ApiSection>
<ApiSection
name="shutdown()"
signature="shutdown(timeout?: number): Promise<void>"
description="Flush all pending traces and tear down the background sender. Default timeout is 10 seconds."
>
<CodeBlock title="shutdown.ts" language="typescript">{`import { shutdown } from "agentlens-sdk";
process.on("SIGTERM", async () => {
await shutdown(30000);
process.exit(0);
});`}</CodeBlock>
</ApiSection>
<section className="mt-12">
<h2 className="text-2xl font-semibold mb-4">Environment Variables</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-800">
<th className="text-left py-2 pr-4 text-neutral-400 font-medium">Variable</th>
<th className="text-left py-2 text-neutral-400 font-medium">Description</th>
</tr>
</thead>
<tbody className="text-neutral-300">
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">AGENTLENS_API_KEY</td>
<td className="py-2">API key (overrides init param)</td>
</tr>
<tr className="border-b border-neutral-800/50">
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">AGENTLENS_ENDPOINT</td>
<td className="py-2">Server URL (overrides init param)</td>
</tr>
<tr>
<td className="py-2 pr-4 font-mono text-emerald-400 text-xs">AGENTLENS_ENABLED</td>
<td className="py-2">Set to &quot;false&quot; to disable</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
);
}

View File

@@ -5,8 +5,63 @@ import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "AgentLens",
description: "Agent observability that traces decisions, not just API calls",
metadataBase: new URL("https://agentlens.vectry.tech"),
title: {
default: "AgentLens — Agent Observability Platform",
template: "%s | AgentLens",
},
description:
"Open-source agent observability that traces decisions, not just API calls. Monitor AI agent reasoning, tool selection, and routing in real-time.",
keywords: [
"agent observability",
"AI monitoring",
"LLM tracing",
"agent decisions",
"AI debugging",
"tool selection tracing",
"multi-agent observability",
"open source",
],
authors: [{ name: "Vectry" }],
creator: "Vectry",
openGraph: {
type: "website",
locale: "en_US",
url: "https://agentlens.vectry.tech",
siteName: "AgentLens",
title: "AgentLens — Agent Observability Platform",
description:
"Open-source agent observability that traces decisions, not just API calls. Monitor AI agent reasoning, tool selection, and routing in real-time.",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "AgentLens — Agent Observability Platform",
},
],
},
twitter: {
card: "summary_large_image",
title: "AgentLens — Agent Observability Platform",
description:
"Open-source agent observability that traces decisions, not just API calls. Monitor AI agent reasoning, tool selection, and routing in real-time.",
images: ["/og-image.png"],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
alternates: {
canonical: "https://agentlens.vectry.tech",
},
};
export default function RootLayout({

View File

@@ -20,6 +20,46 @@ import {
export default function HomePage() {
return (
<div className="min-h-screen bg-neutral-950">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "SoftwareApplication",
name: "AgentLens",
applicationCategory: "DeveloperApplication",
operatingSystem: "Web",
url: "https://agentlens.vectry.tech",
description:
"Open-source agent observability platform that traces AI agent decisions, not just API calls.",
offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
featureList: [
"Agent Decision Tracing",
"Real-time Dashboard",
"OpenAI Integration",
"Anthropic Integration",
"LangChain Integration",
"OpenCode Plugin",
"Self-hosting Support",
"Python SDK",
"TypeScript SDK",
],
}),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "Organization",
name: "Vectry",
url: "https://vectry.tech",
logo: "https://vectry.tech/static/img/logo-icon.png",
sameAs: ["https://gitea.repi.fun/repi/agentlens"],
}),
}}
/>
{/* Hero Section */}
<section className="relative overflow-hidden border-b border-neutral-800/50">
<div className="absolute inset-0 bg-gradient-to-b from-emerald-500/5 via-transparent to-transparent" />

View File

@@ -0,0 +1,22 @@
import { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{ userAgent: "GPTBot", allow: "/" },
{ userAgent: "ChatGPT-User", allow: "/" },
{ userAgent: "ClaudeBot", allow: "/" },
{ userAgent: "PerplexityBot", allow: "/" },
{ userAgent: "Applebot-Extended", allow: "/" },
{ userAgent: "CCBot", disallow: "/" },
{ userAgent: "Google-Extended", disallow: "/" },
{ userAgent: "Bytespider", disallow: "/" },
{
userAgent: "*",
allow: "/",
disallow: ["/api/", "/dashboard/"],
},
],
sitemap: "https://agentlens.vectry.tech/sitemap.xml",
};
}

View File

@@ -0,0 +1,22 @@
import { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = "https://agentlens.vectry.tech";
const now = new Date();
return [
{ url: baseUrl, lastModified: now, changeFrequency: "weekly", priority: 1.0 },
{ url: `${baseUrl}/dashboard`, lastModified: now, changeFrequency: "daily", priority: 0.8 },
{ url: `${baseUrl}/docs`, lastModified: now, changeFrequency: "weekly", priority: 0.9 },
{ url: `${baseUrl}/docs/getting-started`, lastModified: now, changeFrequency: "monthly", priority: 0.9 },
{ url: `${baseUrl}/docs/concepts`, lastModified: now, changeFrequency: "monthly", priority: 0.7 },
{ url: `${baseUrl}/docs/python-sdk`, lastModified: now, changeFrequency: "monthly", priority: 0.8 },
{ url: `${baseUrl}/docs/typescript-sdk`, lastModified: now, changeFrequency: "monthly", priority: 0.8 },
{ url: `${baseUrl}/docs/opencode-plugin`, lastModified: now, changeFrequency: "monthly", priority: 0.8 },
{ url: `${baseUrl}/docs/integrations/openai`, lastModified: now, changeFrequency: "monthly", priority: 0.7 },
{ url: `${baseUrl}/docs/integrations/anthropic`, lastModified: now, changeFrequency: "monthly", priority: 0.7 },
{ url: `${baseUrl}/docs/integrations/langchain`, lastModified: now, changeFrequency: "monthly", priority: 0.7 },
{ url: `${baseUrl}/docs/api-reference`, lastModified: now, changeFrequency: "monthly", priority: 0.7 },
{ url: `${baseUrl}/docs/self-hosting`, lastModified: now, changeFrequency: "monthly", priority: 0.7 },
];
}

View File

@@ -0,0 +1,34 @@
import { codeToHtml } from "shiki";
import { CopyButton } from "./copy-button";
export async function CodeBlock({
children,
title,
language = "text",
}: {
children: string;
title?: string;
language?: string;
}) {
const html = await codeToHtml(children, {
lang: language,
theme: "github-dark",
});
return (
<div className="rounded-xl overflow-hidden border border-neutral-800 bg-neutral-900/50 my-4">
{title && (
<div className="px-4 py-2.5 border-b border-neutral-800 text-xs text-neutral-500 font-mono">
{title}
</div>
)}
<div className="relative">
<CopyButton text={children} />
<div
className="p-4 overflow-x-auto text-sm leading-relaxed [&_pre]:!bg-transparent [&_code]:!bg-transparent"
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,24 @@
"use client";
import { useState } from "react";
import { Copy, Check } from "lucide-react";
export function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<button
onClick={handleCopy}
className="absolute top-3 right-3 p-1.5 rounded-md text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800/50 transition-colors"
aria-label="Copy to clipboard"
>
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
</button>
);
}

View File

@@ -461,13 +461,50 @@ function CostBreakdown({
// Section C: Token Usage Gauge
function TokenUsageGauge({ trace }: { trace: Trace }) {
const tokenData = useMemo(() => {
// Try to get total tokens from various sources
const totalTokens =
(trace.metadata?.totalTokens as number | null | undefined) ??
(trace.metadata?.tokenCount as number | null | undefined) ??
null;
const maxTokens = 128000; // Default context window
const modelContextWindows: Record<string, number> = {
"gpt-5.2": 128000,
"gpt-5.1": 128000,
"gpt-5": 128000,
"gpt-5-mini": 128000,
"gpt-5-nano": 128000,
"gpt-4.1": 1047576,
"gpt-4.1-mini": 1047576,
"gpt-4.1-nano": 1047576,
"o3": 200000,
"o3-mini": 200000,
"o4-mini": 200000,
"gpt-4": 8192,
"gpt-4-32k": 32768,
"gpt-4-turbo": 128000,
"gpt-4o": 128000,
"gpt-4o-mini": 128000,
"gpt-3.5-turbo": 16385,
"claude-opus-4-6": 200000,
"claude-4.5-opus": 200000,
"claude-4.5-sonnet": 200000,
"claude-4.5-haiku": 200000,
"claude-3-opus": 200000,
"claude-3-sonnet": 200000,
"claude-3-haiku": 200000,
"claude-3.5-sonnet": 200000,
"claude-4-opus": 200000,
"claude-4-sonnet": 200000,
};
const model = (trace.metadata?.model as string | undefined) ?? "";
const modelLower = model.toLowerCase();
let maxTokens = 128000;
for (const [prefix, ctx] of Object.entries(modelContextWindows)) {
if (modelLower.startsWith(prefix)) {
maxTokens = ctx;
break;
}
}
return {
totalTokens,

View File

@@ -18,13 +18,19 @@ import {
DollarSign,
AlertCircle,
Terminal,
Cpu,
FileEdit,
Search,
Eye,
FolderOpen,
FilePlus,
} from "lucide-react";
import { cn, formatDuration, formatRelativeTime } from "@/lib/utils";
import { DecisionTree } from "./decision-tree";
import { TraceAnalytics } from "./trace-analytics";
type TraceStatus = "RUNNING" | "COMPLETED" | "ERROR";
type TabType = "tree" | "analytics" | "decisions" | "spans" | "events";
type TabType = "tree" | "analytics" | "decisions" | "spans" | "events" | "agent";
interface DecisionPoint {
id: string;
@@ -278,6 +284,14 @@ export function TraceDetail({
icon={Activity}
label={`Events (${events.length})`}
/>
{trace.tags.includes("opencode") && (
<TabButton
active={activeTab === "agent"}
onClick={() => setActiveTab("agent")}
icon={Cpu}
label="Agent"
/>
)}
</div>
</div>
@@ -304,6 +318,9 @@ export function TraceDetail({
)}
{activeTab === "spans" && <SpansTab spans={spans} />}
{activeTab === "events" && <EventsTab events={events} />}
{activeTab === "agent" && (
<CodingAgentTab spans={spans} events={events} />
)}
</div>
</div>
);
@@ -566,3 +583,239 @@ function EventsTab({ events }: { events: Event[] }) {
</div>
);
}
const toolCategoryColors: Record<string, { bar: string; label: string }> = {
file: { bar: "bg-blue-500", label: "text-blue-400" },
search: { bar: "bg-purple-500", label: "text-purple-400" },
shell: { bar: "bg-amber-500", label: "text-amber-400" },
lsp: { bar: "bg-emerald-500", label: "text-emerald-400" },
other: { bar: "bg-neutral-500", label: "text-neutral-400" },
};
function getToolCategory(toolName: string): string {
const lower = toolName.toLowerCase();
if (["read", "write", "edit", "glob", "file"].some((k) => lower.includes(k))) return "file";
if (["grep", "search", "find"].some((k) => lower.includes(k))) return "search";
if (["bash", "shell", "terminal", "exec"].some((k) => lower.includes(k))) return "shell";
if (["lsp", "diagnostics", "definition", "references", "symbols", "rename"].some((k) => lower.includes(k))) return "lsp";
return "other";
}
function truncateFilePath(filePath: string): string {
const segments = filePath.replace(/\\/g, "/").split("/").filter(Boolean);
if (segments.length <= 3) return segments.join("/");
return ".../" + segments.slice(-3).join("/");
}
type FileInteractionKind = "read" | "edit" | "create";
interface FileInteraction {
kind: FileInteractionKind;
filePath: string;
timestamp: string;
source: string;
}
function getFileInteractionIcon(kind: FileInteractionKind) {
switch (kind) {
case "read":
return Eye;
case "edit":
return FileEdit;
case "create":
return FilePlus;
}
}
function getFileInteractionColor(kind: FileInteractionKind): string {
switch (kind) {
case "read":
return "text-blue-400 bg-blue-500/10 border-blue-500/20";
case "edit":
return "text-amber-400 bg-amber-500/10 border-amber-500/20";
case "create":
return "text-emerald-400 bg-emerald-500/10 border-emerald-500/20";
}
}
function CodingAgentTab({ spans, events }: { spans: Span[]; events: Event[] }) {
const toolCallSpans = spans.filter((s) => s.type === "TOOL_CALL");
const toolCounts: Record<string, number> = {};
for (const span of toolCallSpans) {
toolCounts[span.name] = (toolCounts[span.name] || 0) + 1;
}
const sortedTools = Object.entries(toolCounts).sort((a, b) => b[1] - a[1]);
const maxToolCount = sortedTools.length > 0 ? sortedTools[0][1] : 0;
const fileInteractions: FileInteraction[] = [];
for (const event of events) {
if (event.name === "file.edited" && event.metadata.filePath) {
fileInteractions.push({
kind: "edit",
filePath: String(event.metadata.filePath),
timestamp: event.timestamp,
source: "event",
});
}
}
const fileToolPatterns: Record<string, FileInteractionKind> = {
read: "read",
glob: "read",
grep: "read",
edit: "edit",
write: "create",
};
for (const span of toolCallSpans) {
const lower = span.name.toLowerCase();
for (const [pattern, kind] of Object.entries(fileToolPatterns)) {
if (lower.includes(pattern)) {
let filePath = "";
if (span.metadata.filePath) {
filePath = String(span.metadata.filePath);
} else if (span.input && typeof span.input === "object" && span.input !== null) {
const inputObj = span.input as Record<string, unknown>;
if (inputObj.filePath) filePath = String(inputObj.filePath);
else if (inputObj.path) filePath = String(inputObj.path);
else if (inputObj.pattern) filePath = String(inputObj.pattern);
}
if (filePath) {
fileInteractions.push({
kind,
filePath,
timestamp: span.startedAt,
source: span.name,
});
}
break;
}
}
}
fileInteractions.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
const hasData = sortedTools.length > 0 || fileInteractions.length > 0;
if (!hasData) {
return (
<div className="flex flex-col items-center justify-center py-20">
<div className="p-4 rounded-2xl bg-neutral-800/50 mb-4">
<Cpu className="w-8 h-8 text-neutral-600" />
</div>
<p className="text-neutral-500 text-sm">No coding agent data available</p>
</div>
);
}
return (
<div className="space-y-6">
{sortedTools.length > 0 && (
<div className="bg-neutral-900 border border-neutral-800 rounded-xl p-5">
<div className="flex items-center gap-3 mb-5">
<div className="p-2 rounded-lg bg-blue-500/10">
<Terminal className="w-4 h-4 text-blue-400" />
</div>
<h3 className="text-sm font-semibold text-neutral-200 uppercase tracking-wider">
Tool Usage Breakdown
</h3>
<span className="ml-auto text-xs text-neutral-500">
{toolCallSpans.length} total calls
</span>
</div>
<div className="space-y-2.5">
{sortedTools.map(([toolName, count]) => {
const category = getToolCategory(toolName);
const colors = toolCategoryColors[category];
const widthPercent = maxToolCount > 0 ? (count / maxToolCount) * 100 : 0;
return (
<div key={toolName} className="group">
<div className="flex items-center gap-3">
<span className={cn("text-xs font-mono w-36 truncate shrink-0", colors.label)} title={toolName}>
{toolName}
</span>
<div className="flex-1 h-6 bg-neutral-800/60 rounded-md overflow-hidden relative">
<div
className={cn("h-full rounded-md transition-all duration-500 ease-out opacity-80 group-hover:opacity-100", colors.bar)}
style={{ width: `${Math.max(widthPercent, 2)}%` }}
/>
</div>
<span className="text-xs font-mono text-neutral-300 w-8 text-right tabular-nums">
{count}
</span>
</div>
</div>
);
})}
</div>
<div className="flex items-center gap-4 mt-5 pt-4 border-t border-neutral-800">
{Object.entries(toolCategoryColors).map(([category, colors]) => {
const categoryCount = sortedTools
.filter(([name]) => getToolCategory(name) === category)
.reduce((sum, [, c]) => sum + c, 0);
if (categoryCount === 0) return null;
return (
<div key={category} className="flex items-center gap-1.5">
<div className={cn("w-2.5 h-2.5 rounded-sm", colors.bar)} />
<span className="text-xs text-neutral-500 capitalize">{category}</span>
</div>
);
})}
</div>
</div>
)}
{fileInteractions.length > 0 && (
<div className="bg-neutral-900 border border-neutral-800 rounded-xl p-5">
<div className="flex items-center gap-3 mb-5">
<div className="p-2 rounded-lg bg-amber-500/10">
<FolderOpen className="w-4 h-4 text-amber-400" />
</div>
<h3 className="text-sm font-semibold text-neutral-200 uppercase tracking-wider">
File Changes Timeline
</h3>
<span className="ml-auto text-xs text-neutral-500">
{fileInteractions.length} interactions
</span>
</div>
<div className="relative">
<div className="absolute left-[15px] top-2 bottom-2 w-px bg-neutral-800" />
<div className="space-y-1">
{fileInteractions.map((interaction, idx) => {
const InteractionIcon = getFileInteractionIcon(interaction.kind);
const colorClass = getFileInteractionColor(interaction.kind);
return (
<div
key={`${interaction.filePath}-${idx}`}
className="flex items-center gap-3 pl-1 py-1.5 group"
>
<div className={cn("relative z-10 p-1.5 rounded-md border shrink-0", colorClass)}>
<InteractionIcon className="w-3 h-3" />
</div>
<div className="flex-1 min-w-0 flex items-center gap-2">
<span className="text-xs font-mono text-neutral-300 truncate" title={interaction.filePath}>
{truncateFilePath(interaction.filePath)}
</span>
<span className={cn(
"shrink-0 px-1.5 py-0.5 rounded text-[10px] font-medium uppercase tracking-wide border",
colorClass
)}>
{interaction.kind}
</span>
</div>
<span className="text-[11px] text-neutral-600 shrink-0 tabular-nums">
{formatRelativeTime(interaction.timestamp)}
</span>
</div>
);
})}
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,347 @@
"""
AgentLens Real LLM Test — MoonshotAI (Kimi) via OpenAI-compatible API.
Tests the full pipeline: SDK → wrap_openai() → real LLM completion → AgentLens dashboard.
Uses MoonshotAI (OpenAI-compatible) with kimi-k2-turbo-preview model.
Usage:
pip install vectry-agentlens openai
python moonshot_real_test.py
"""
import time
import json
import agentlens
from agentlens.integrations.openai import wrap_openai
import openai
# ── Config ──────────────────────────────────────────────────────────
MOONSHOT_API_KEY = "sk-2uhpGUeqISKtiGwd14aGuYJ4tt2p0Ad98qke9T8Ykdc4dEPp"
MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1"
MOONSHOT_MODEL = "kimi-k2-turbo-preview"
AGENTLENS_ENDPOINT = "https://agentlens.vectry.tech"
AGENTLENS_API_KEY = "test-moonshot-key"
# ── Initialize ──────────────────────────────────────────────────────
print("=" * 60)
print("AgentLens Real LLM Test — MoonshotAI (Kimi)")
print("=" * 60)
agentlens.init(
api_key=AGENTLENS_API_KEY,
endpoint=AGENTLENS_ENDPOINT,
)
print(f"[✓] AgentLens initialized → {AGENTLENS_ENDPOINT}")
# Create OpenAI client pointing to MoonshotAI
client = openai.OpenAI(
api_key=MOONSHOT_API_KEY,
base_url=MOONSHOT_BASE_URL,
)
wrap_openai(client)
print(f"[✓] OpenAI client wrapped → {MOONSHOT_BASE_URL}")
print(f"[✓] Model: {MOONSHOT_MODEL}")
print()
# ── Test 1: Basic Completion ────────────────────────────────────────
print("─── Test 1: Basic Completion ───")
with agentlens.trace(
"moonshot-basic-completion",
tags=["moonshot", "test", "basic"],
metadata={"provider": "moonshot", "model": MOONSHOT_MODEL, "test": "basic"},
):
agentlens.log_decision(
type="TOOL_SELECTION",
chosen={
"name": MOONSHOT_MODEL,
"confidence": 0.95,
"params": {"temperature": 0.7, "max_tokens": 200},
},
alternatives=[
{
"name": "moonshot-v1-8k",
"confidence": 0.6,
"reason_rejected": "Older model, less capable",
}
],
reasoning="Using kimi-k2-turbo-preview for best quality/speed balance.",
)
start = time.time()
response = client.chat.completions.create(
model=MOONSHOT_MODEL,
messages=[
{
"role": "system",
"content": "You are a helpful AI assistant. Be concise.",
},
{
"role": "user",
"content": "What are the 3 most important principles of software engineering? Answer in one sentence each.",
},
],
temperature=0.7,
max_tokens=200,
)
elapsed = time.time() - start
content = response.choices[0].message.content
usage = response.usage
print(f" Response ({elapsed:.2f}s):")
print(f" {content[:200]}...")
print(
f" Tokens: {usage.prompt_tokens} in / {usage.completion_tokens} out / {usage.total_tokens} total"
)
print()
# ── Test 2: Multi-turn Conversation with Decision Logging ──────────
print("─── Test 2: Multi-turn with Decisions ───")
with agentlens.trace(
"moonshot-multi-turn-agent",
tags=["moonshot", "test", "multi-turn", "agent"],
metadata={"provider": "moonshot", "model": MOONSHOT_MODEL, "test": "multi-turn"},
):
# Step 1: Classify user intent
agentlens.log_decision(
type="PLANNING",
chosen={
"name": "classify-then-respond",
"confidence": 0.9,
"params": {"strategy": "two-step"},
},
alternatives=[
{
"name": "direct-response",
"confidence": 0.5,
"reason_rejected": "Classification first improves response quality",
}
],
reasoning="Two-step approach: classify intent first, then generate targeted response.",
context_snapshot={"user_query": "Help me debug a Python TypeError"},
)
with agentlens.trace("classify-intent", tags=["classification"]):
classification = client.chat.completions.create(
model=MOONSHOT_MODEL,
messages=[
{
"role": "system",
"content": "Classify the user's programming question into one category: 'syntax', 'runtime', 'logic', 'design', 'performance'. Reply with just the category.",
},
{
"role": "user",
"content": "I'm getting a TypeError: unsupported operand type(s) for +: 'int' and 'str' in my Python code",
},
],
temperature=0.2,
max_tokens=20,
)
category = classification.choices[0].message.content.strip()
print(f" Intent classified: {category}")
# Step 2: Route to appropriate response strategy
agentlens.log_decision(
type="ROUTING",
chosen={
"name": f"respond-as-{category}",
"confidence": 0.85,
},
alternatives=[
{
"name": "generic-response",
"confidence": 0.3,
"reason_rejected": "Classified response is more helpful",
}
],
reasoning=f"User question classified as '{category}' — routing to specialized response.",
context_snapshot={"category": category},
)
# Step 3: Generate response
with agentlens.trace("generate-response", tags=["response"]):
response = client.chat.completions.create(
model=MOONSHOT_MODEL,
messages=[
{
"role": "system",
"content": f"You are an expert Python debugger specializing in {category} errors. Give a concise, actionable fix.",
},
{
"role": "user",
"content": "I'm getting a TypeError: unsupported operand type(s) for +: 'int' and 'str' in my Python code",
},
{
"role": "assistant",
"content": f"This is a {category} error. Let me help you fix it.",
},
{
"role": "user",
"content": "Here's my code: total = count + name where count=5 and name='hello'",
},
],
temperature=0.5,
max_tokens=300,
)
answer = response.choices[0].message.content
print(f" Response: {answer[:150]}...")
print()
# ── Test 3: Tool/Function Calling ───────────────────────────────────
print("─── Test 3: Function Calling ───")
with agentlens.trace(
"moonshot-function-calling",
tags=["moonshot", "test", "tools", "function-calling"],
metadata={
"provider": "moonshot",
"model": MOONSHOT_MODEL,
"test": "function-calling",
},
):
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name, e.g. 'San Francisco'",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit",
},
},
"required": ["location"],
},
},
},
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the web for information",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query",
},
},
"required": ["query"],
},
},
},
]
agentlens.log_decision(
type="TOOL_SELECTION",
chosen={
"name": "provide-tools",
"confidence": 0.9,
"params": {"tools": ["get_weather", "search_web"]},
},
alternatives=[],
reasoning="User query likely requires weather data — providing weather and search tools.",
)
response = client.chat.completions.create(
model=MOONSHOT_MODEL,
messages=[
{
"role": "system",
"content": "You are a helpful assistant with access to tools. Use them when needed.",
},
{"role": "user", "content": "What's the weather like in Lisbon today?"},
],
tools=tools,
temperature=0.3,
max_tokens=200,
)
message = response.choices[0].message
if message.tool_calls:
print(f" Tool calls requested: {len(message.tool_calls)}")
for tc in message.tool_calls:
print(f"{tc.function.name}({tc.function.arguments})")
# Simulate tool response
agentlens.log_decision(
type="TOOL_SELECTION",
chosen={
"name": tc.function.name,
"confidence": 1.0,
},
alternatives=[],
reasoning=f"Model requested {tc.function.name} — executing tool call.",
)
# Send fake tool result back
tool_result = json.dumps(
{
"temperature": 18,
"unit": "celsius",
"condition": "sunny",
"location": "Lisbon",
}
)
final_response = client.chat.completions.create(
model=MOONSHOT_MODEL,
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What's the weather like in Lisbon today?"},
message,
{
"role": "tool",
"tool_call_id": message.tool_calls[0].id,
"content": tool_result,
},
],
temperature=0.5,
max_tokens=200,
)
print(f" Final answer: {final_response.choices[0].message.content[:150]}...")
else:
print(f" Direct response (no tool calls): {message.content[:150]}...")
print()
# ── Shutdown & Verify ───────────────────────────────────────────────
print("─── Flushing traces to AgentLens... ───")
agentlens.shutdown()
print("[✓] All traces flushed")
# Wait a moment for async processing
time.sleep(2)
# Verify traces arrived
print()
print("─── Verifying traces in dashboard... ───")
import httpx
resp = httpx.get(
f"{AGENTLENS_ENDPOINT}/api/traces",
params={"search": "moonshot", "limit": "10"},
headers={"Authorization": f"Bearer {AGENTLENS_API_KEY}"},
)
if resp.status_code == 200:
data = resp.json()
traces = data.get("traces", [])
print(f"[✓] Found {len(traces)} moonshot traces in dashboard:")
for t in traces:
spans = t.get("_count", {}).get("spans", "?")
decisions = t.get("_count", {}).get("decisionPoints", "?")
print(
f"{t['name']} — status={t['status']}, spans={spans}, decisions={decisions}"
)
else:
print(f"[✗] API returned {resp.status_code}: {resp.text[:200]}")
print()
print("=" * 60)
print("Test complete! Visit https://agentlens.vectry.tech/dashboard")
print("=" * 60)

1988
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
# opencode-agentlens
OpenCode plugin for AgentLens — trace your coding agent's decisions, tool calls, and sessions.
[![npm version](https://img.shields.io/npm/v/opencode-agentlens.svg)](https://www.npmjs.com/package/opencode-agentlens)
[![license](https://img.shields.io/npm/l/opencode-agentlens.svg)](https://github.com/repi/agentlens/blob/main/LICENSE)
## Requirements
- OpenCode >= 1.1.0
## Install
```bash
npm install opencode-agentlens
```
## Configuration
### Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
| `AGENTLENS_API_KEY` | Yes | — | Your AgentLens API key. |
| `AGENTLENS_ENDPOINT` | No | AgentLens cloud | API endpoint URL. |
| `AGENTLENS_ENABLED` | No | `true` | Set to `false` to disable tracing. |
| `AGENTLENS_CAPTURE_CONTENT` | No | `true` | Capture message and tool output content. |
| `AGENTLENS_MAX_OUTPUT_LENGTH` | No | `10000` | Max characters to capture per output. |
| `AGENTLENS_FLUSH_INTERVAL` | No | `5000` | Flush interval in milliseconds. |
| `AGENTLENS_BATCH_SIZE` | No | `100` | Max items per batch before auto-flush. |
### OpenCode Setup
Add the plugin to your OpenCode configuration at `~/.config/opencode/opencode.json`:
```json
{
"plugins": [
{
"name": "agentlens",
"module": "opencode-agentlens"
}
]
}
```
Set your API key:
```bash
export AGENTLENS_API_KEY="your-api-key"
```
The plugin activates automatically when OpenCode starts. No code changes required.
## What Gets Captured
The plugin hooks into OpenCode's event system and records:
- **Sessions** — Full session lifecycle from start to finish, including duration and metadata.
- **Tool calls** — Every tool invocation with input arguments and output results (e.g., file reads, shell commands, code edits).
- **LLM calls** — Chat messages sent to and received from the model, including token usage.
- **Permission flows** — When the agent requests permission and whether it was granted or denied.
- **File edits** — File paths and change summaries produced by the agent.
All data is sent to your AgentLens instance where you can inspect traces, replay sessions, and analyze agent behavior.
## How It Works
The plugin registers handlers for OpenCode's event hooks:
| Event | What is recorded |
|---|---|
| Session start/end | Trace lifecycle, session metadata |
| `tool.execute.before` | Tool name, input arguments |
| `tool.execute.after` | Tool output, duration, success/failure |
| `chat.message` | LLM responses and assistant messages |
| `chat.params` | Model parameters and prompt configuration |
| `permission.ask` | Permission requests and user decisions |
Each OpenCode session maps to a single AgentLens trace. Tool calls and LLM interactions become spans within that trace.
## Documentation
Full documentation: [agentlens.vectry.tech/docs/opencode-plugin](https://agentlens.vectry.tech/docs/opencode-plugin)
## License
MIT

View File

@@ -0,0 +1,57 @@
{
"name": "opencode-agentlens",
"version": "0.1.0",
"description": "OpenCode plugin for AgentLens — trace your coding agent's decisions, tool calls, and sessions",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"files": ["dist"],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
},
"dependencies": {
"agentlens-sdk": "*"
},
"devDependencies": {
"@opencode-ai/plugin": "^1.1.53",
"tsup": "^8.3.0",
"typescript": "^5.7.0"
},
"peerDependencies": {
"@opencode-ai/plugin": ">=1.1.0"
},
"keywords": [
"opencode",
"agentlens",
"observability",
"tracing",
"coding-agent"
],
"license": "MIT",
"author": "Vectry <hunter@repi.fun>",
"repository": {
"type": "git",
"url": "https://gitea.repi.fun/repi/agentlens",
"directory": "packages/opencode-plugin"
},
"homepage": "https://agentlens.vectry.tech",
"bugs": {
"url": "https://gitea.repi.fun/repi/agentlens/issues"
}
}

View File

@@ -0,0 +1,41 @@
export interface PluginConfig {
apiKey: string;
endpoint: string;
enabled: boolean;
/** Opt-in: capture full message content in traces */
captureContent: boolean;
/** Maximum characters for tool output before truncation */
maxOutputLength: number;
/** Milliseconds between automatic flushes */
flushInterval: number;
/** Maximum traces per batch */
maxBatchSize: number;
}
export function loadConfig(): PluginConfig {
const apiKey = process.env["AGENTLENS_API_KEY"] ?? "";
if (!apiKey) {
console.warn(
"[agentlens] AGENTLENS_API_KEY not set — plugin will be disabled",
);
}
return {
apiKey,
endpoint:
process.env["AGENTLENS_ENDPOINT"] ?? "https://agentlens.vectry.tech",
enabled: (process.env["AGENTLENS_ENABLED"] ?? "true") === "true",
captureContent:
(process.env["AGENTLENS_CAPTURE_CONTENT"] ?? "false") === "true",
maxOutputLength: parseInt(
process.env["AGENTLENS_MAX_OUTPUT_LENGTH"] ?? "2000",
10,
),
flushInterval: parseInt(
process.env["AGENTLENS_FLUSH_INTERVAL"] ?? "5000",
10,
),
maxBatchSize: parseInt(process.env["AGENTLENS_BATCH_SIZE"] ?? "10", 10),
};
}

View File

@@ -0,0 +1,146 @@
import type { Plugin } from "@opencode-ai/plugin";
import type { JsonValue } from "agentlens-sdk";
import { init, flush, EventType as EventTypeValues } from "agentlens-sdk";
import { loadConfig } from "./config.js";
import { SessionState } from "./state.js";
import { truncate, safeJsonValue } from "./utils.js";
const plugin: Plugin = async ({ project, directory, worktree }) => {
const config = loadConfig();
if (!config.enabled || !config.apiKey) {
console.log("[agentlens] Plugin disabled — missing AGENTLENS_API_KEY");
return {};
}
init({
apiKey: config.apiKey,
endpoint: config.endpoint,
flushInterval: config.flushInterval,
maxBatchSize: config.maxBatchSize,
});
const state = new SessionState();
return {
event: async ({ event }) => {
const type = event.type;
const props = (event as Record<string, unknown>).properties as
| Record<string, unknown>
| undefined;
if (type === "session.created" && props?.["id"]) {
state.startSession(String(props["id"]), {
project: project.id,
directory,
worktree,
});
}
if (type === "session.idle") {
const sessionId = props?.["sessionID"] ?? props?.["id"];
if (sessionId) await flush();
}
if (type === "session.error") {
const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
if (sessionId) {
const trace = state.getTrace(sessionId);
if (trace) {
trace.addEvent({
type: EventTypeValues.ERROR,
name: String(props?.["error"] ?? "session error"),
metadata: safeJsonValue(props) as JsonValue,
});
}
}
}
if (type === "session.deleted") {
const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
if (sessionId) state.endSession(sessionId);
}
if (type === "session.diff") {
const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
if (sessionId) {
const trace = state.getTrace(sessionId);
if (trace) {
trace.setMetadata({
diff: truncate(String(props?.["diff"] ?? ""), 5000),
});
}
}
}
if (type === "file.edited") {
const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
const trace = sessionId ? state.getTrace(sessionId) : undefined;
if (trace) {
trace.addEvent({
type: EventTypeValues.CUSTOM,
name: "file.edited",
metadata: safeJsonValue({
filePath: props?.["filePath"],
}) as JsonValue,
});
}
}
},
"tool.execute.before": async (input, output) => {
state.startToolCall(
input.callID,
input.tool,
output.args as unknown,
input.sessionID,
);
},
"tool.execute.after": async (input, output) => {
state.endToolCall(
input.callID,
truncate(output.output ?? "", config.maxOutputLength),
output.title ?? input.tool,
output.metadata as unknown,
);
},
"chat.message": async (input) => {
if (input.model) {
state.recordLLMCall(input.sessionID, {
model: input.model,
agent: input.agent,
messageID: input.messageID,
});
}
},
"chat.params": async (input, output) => {
const trace = state.getTrace(input.sessionID);
if (trace) {
trace.addEvent({
type: EventTypeValues.CUSTOM,
name: "chat.params",
metadata: safeJsonValue({
agent: input.agent,
model: input.model.id,
provider: input.provider.info.id,
temperature: output.temperature,
topP: output.topP,
topK: output.topK,
}) as JsonValue,
});
}
},
"permission.ask": async (input, output) => {
state.recordPermission(input.sessionID, input, output.status);
},
};
};
export default plugin;
export { plugin as AgentLensPlugin };
export type { PluginConfig } from "./config.js";
export { loadConfig } from "./config.js";

View File

@@ -0,0 +1,183 @@
import {
TraceBuilder,
SpanType,
SpanStatus,
DecisionType,
nowISO,
} from "agentlens-sdk";
import type { JsonValue, TraceStatus } from "agentlens-sdk";
import { extractToolMetadata, safeJsonValue } from "./utils.js";
interface ToolCallState {
startTime: number;
tool: string;
args: unknown;
sessionID: string;
}
export class SessionState {
private traces = new Map<string, TraceBuilder>();
private toolCalls = new Map<string, ToolCallState>();
private rootSpans = new Map<string, string>();
startSession(
sessionId: string,
metadata?: Record<string, unknown>,
): TraceBuilder {
const trace = new TraceBuilder("opencode-session", {
sessionId,
tags: ["opencode", "coding-agent"],
metadata: metadata ? (safeJsonValue(metadata) as JsonValue) : undefined,
});
const rootSpanId = trace.addSpan({
name: "session",
type: SpanType.AGENT,
startedAt: nowISO(),
metadata: metadata ? (safeJsonValue(metadata) as JsonValue) : undefined,
});
this.traces.set(sessionId, trace);
this.rootSpans.set(sessionId, rootSpanId);
return trace;
}
getTrace(sessionId: string): TraceBuilder | undefined {
return this.traces.get(sessionId);
}
endSession(sessionId: string, status?: TraceStatus): void {
const trace = this.traces.get(sessionId);
if (!trace) return;
const rootSpanId = this.rootSpans.get(sessionId);
if (rootSpanId) {
trace.addSpan({
id: rootSpanId,
name: "session",
type: SpanType.AGENT,
status: status === "ERROR" ? SpanStatus.ERROR : SpanStatus.COMPLETED,
endedAt: nowISO(),
});
}
trace.end({ status: status ?? "COMPLETED" });
this.traces.delete(sessionId);
this.rootSpans.delete(sessionId);
}
startToolCall(
callID: string,
tool: string,
args: unknown,
sessionID: string,
): void {
this.toolCalls.set(callID, {
startTime: Date.now(),
tool,
args,
sessionID,
});
}
endToolCall(
callID: string,
output: string,
title: string,
metadata: unknown,
): void {
const call = this.toolCalls.get(callID);
if (!call) return;
this.toolCalls.delete(callID);
const trace = this.traces.get(call.sessionID);
if (!trace) return;
const durationMs = Date.now() - call.startTime;
const rootSpanId = this.rootSpans.get(call.sessionID);
const toolMeta = extractToolMetadata(call.tool, call.args);
trace.addSpan({
name: title,
type: SpanType.TOOL_CALL,
parentSpanId: rootSpanId,
input: safeJsonValue(call.args),
output: output as JsonValue,
durationMs,
status: SpanStatus.COMPLETED,
startedAt: new Date(call.startTime).toISOString(),
endedAt: nowISO(),
metadata: safeJsonValue({ ...toolMeta, rawMetadata: metadata }),
});
trace.addDecision({
type: DecisionType.TOOL_SELECTION,
chosen: call.tool as JsonValue,
alternatives: [],
reasoning: title,
durationMs,
parentSpanId: rootSpanId,
});
}
recordLLMCall(
sessionId: string,
options: {
model?: { providerID: string; modelID: string };
agent?: string;
messageID?: string;
},
): void {
const trace = this.traces.get(sessionId);
if (!trace) return;
const rootSpanId = this.rootSpans.get(sessionId);
const agentName = options.agent ?? "assistant";
const modelName = options.model?.modelID ?? "unknown";
trace.addSpan({
name: `${agentName}${modelName}`,
type: SpanType.LLM_CALL,
parentSpanId: rootSpanId,
status: SpanStatus.COMPLETED,
startedAt: nowISO(),
endedAt: nowISO(),
metadata: safeJsonValue({
provider: options.model?.providerID,
model: options.model?.modelID,
agent: options.agent,
messageID: options.messageID,
}),
});
}
recordPermission(
sessionId: string,
permission: unknown,
status: string,
): void {
const trace = this.traces.get(sessionId);
if (!trace) return;
const rootSpanId = this.rootSpans.get(sessionId);
const p = permission as Record<string, unknown> | null;
const title = (p?.["title"] as string) ?? "permission";
const permType = (p?.["type"] as string) ?? "unknown";
trace.addDecision({
type: DecisionType.ESCALATION,
chosen: safeJsonValue({ action: status }),
alternatives: [
"allow" as JsonValue,
"deny" as JsonValue,
"ask" as JsonValue,
],
reasoning: `${permType}: ${title}`,
parentSpanId: rootSpanId,
});
}
getRootSpanId(sessionId: string): string | undefined {
return this.rootSpans.get(sessionId);
}
}

View File

@@ -0,0 +1,116 @@
import type { JsonValue } from "agentlens-sdk";
export function truncate(str: string, maxLength: number): string {
if (str.length <= maxLength) return str;
return str.slice(0, maxLength) + "... [truncated]";
}
export function extractToolMetadata(
tool: string,
args: unknown,
): Record<string, unknown> {
const a = args as Record<string, unknown> | null | undefined;
if (!a || typeof a !== "object") return {};
switch (tool) {
case "read":
case "mcp_read":
return { filePath: a["filePath"] };
case "write":
case "mcp_write":
return { filePath: a["filePath"] };
case "edit":
case "mcp_edit":
return { filePath: a["filePath"] };
case "bash":
case "mcp_bash":
return {
command: truncate(String(a["command"] ?? ""), 200),
};
case "glob":
case "mcp_glob":
return { pattern: a["pattern"] };
case "grep":
case "mcp_grep":
return { pattern: a["pattern"], path: a["path"] };
case "task":
case "mcp_task":
return {
category: a["category"],
description: a["description"],
};
default:
return {};
}
}
const MODEL_COSTS: Record<string, { input: number; output: number }> = {
"gpt-5.2": { input: 1.75, output: 14 },
"gpt-5.1": { input: 1.25, output: 10 },
"gpt-5": { input: 1.25, output: 10 },
"gpt-5-mini": { input: 0.25, output: 2 },
"gpt-5-nano": { input: 0.05, output: 0.4 },
"gpt-4.1": { input: 2, output: 8 },
"gpt-4.1-mini": { input: 0.4, output: 1.6 },
"gpt-4.1-nano": { input: 0.1, output: 0.4 },
"o3": { input: 2, output: 8 },
"o3-mini": { input: 1.1, output: 4.4 },
"o4-mini": { input: 1.1, output: 4.4 },
"o1": { input: 15, output: 60 },
"gpt-4o": { input: 2.5, output: 10 },
"gpt-4o-mini": { input: 0.15, output: 0.6 },
"gpt-4-turbo": { input: 10, output: 30 },
"gpt-4": { input: 30, output: 60 },
"claude-opus-4-6": { input: 5, output: 25 },
"claude-opus-4-20250514": { input: 15, output: 75 },
"claude-sonnet-4-20250514": { input: 3, output: 15 },
"claude-4.5-opus": { input: 5, output: 25 },
"claude-4.5-sonnet": { input: 3, output: 15 },
"claude-4.5-haiku": { input: 1, output: 5 },
"claude-3-5-sonnet": { input: 3, output: 15 },
"claude-3-5-haiku": { input: 0.8, output: 4 },
"claude-3-opus": { input: 15, output: 75 },
"claude-3-haiku": { input: 0.25, output: 1.25 },
};
export function getModelCost(
modelId: string,
): { input: number; output: number } | undefined {
const direct = MODEL_COSTS[modelId];
if (direct) return direct;
for (const [key, cost] of Object.entries(MODEL_COSTS)) {
if (modelId.includes(key)) return cost;
}
return undefined;
}
/** Coerce arbitrary values into SDK-compatible `JsonValue`, stringifying unknowns. */
export function safeJsonValue(value: unknown): JsonValue {
if (value === null || value === undefined) return null;
if (
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean"
) {
return value;
}
if (Array.isArray(value)) {
return value.map((v) => safeJsonValue(v));
}
if (typeof value === "object") {
const result: Record<string, JsonValue> = {};
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
result[k] = safeJsonValue(v);
}
return result;
}
return String(value);
}

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,9 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm", "cjs"],
dts: true,
clean: true,
sourcemap: true,
});

View File

@@ -1 +1,8 @@
"""Integration packages for AgentLens."""
"""Integration packages for AgentLens.
Available integrations:
- ``openai``: Wrap OpenAI clients with ``wrap_openai(client)``.
- ``anthropic``: Wrap Anthropic clients with ``wrap_anthropic(client)``.
- ``langchain``: LangChain callback handler for tracing.
"""

View File

@@ -0,0 +1,702 @@
"""Anthropic integration for AgentLens.
This module provides a wrapper that auto-instruments Anthropic API calls with
tracing, span creation, decision logging for tool calls, and token tracking.
"""
import json
import logging
import time
from functools import wraps
from typing import Any, Dict, Iterator, List, Optional
from agentlens.models import (
Event,
EventType,
_now_iso,
)
from agentlens.trace import (
TraceContext,
_get_context_stack,
get_current_span_id,
get_current_trace,
)
logger = logging.getLogger("agentlens")
# Cost per 1K tokens (input/output) for common Claude models
_MODEL_COSTS: Dict[str, tuple] = {
# Claude 4.5 family
"claude-opus-4-6": (0.005, 0.025),
"claude-4.5-opus": (0.005, 0.025),
"claude-4.5-sonnet": (0.003, 0.015),
"claude-4.5-haiku": (0.001, 0.005),
# Claude 4 family
"claude-sonnet-4-20250514": (0.003, 0.015),
"claude-opus-4-20250514": (0.015, 0.075),
# Claude 3.5 family
"claude-3-5-sonnet-20240620": (0.003, 0.015),
"claude-3-5-sonnet-20241022": (0.003, 0.015),
"claude-3-5-haiku-20241022": (0.0008, 0.004),
# Claude 3 family
"claude-3-opus-20240229": (0.015, 0.075),
"claude-3-sonnet-20240229": (0.003, 0.015),
"claude-3-haiku-20240307": (0.00025, 0.00125),
# Short aliases for prefix matching
"claude-3-opus": (0.015, 0.075),
"claude-3-sonnet": (0.003, 0.015),
"claude-3-haiku": (0.00025, 0.00125),
"claude-3-5-sonnet": (0.003, 0.015),
"claude-3-5-haiku": (0.0008, 0.004),
"claude-3.5-sonnet": (0.003, 0.015),
"claude-3.5-haiku": (0.0008, 0.004),
"claude-sonnet-4": (0.003, 0.015),
"claude-opus-4": (0.005, 0.025),
"claude-4-sonnet": (0.003, 0.015),
"claude-4-opus": (0.005, 0.025),
}
def _truncate_data(data: Any, max_length: int = 500) -> Any:
"""Truncate data for privacy while preserving structure."""
if isinstance(data, str):
return data[:max_length] + "..." if len(data) > max_length else data
elif isinstance(data, dict):
return {k: _truncate_data(v, max_length) for k, v in data.items()}
elif isinstance(data, list):
return [_truncate_data(item, max_length) for item in data]
else:
return data
def _calculate_cost(
model: str, input_tokens: int, output_tokens: int
) -> Optional[float]:
"""Calculate cost in USD based on model pricing."""
model_lower = model.lower()
if model_lower in _MODEL_COSTS:
input_cost, output_cost = _MODEL_COSTS[model_lower]
return (float(input_tokens) / 1000.0) * input_cost + float(
output_tokens
) / 1000.0 * output_cost
best_match = None
best_len = 0
for model_name, costs in _MODEL_COSTS.items():
if model_lower.startswith(model_name.lower()) and len(model_name) > best_len:
best_match = costs
best_len = len(model_name)
if best_match:
input_cost, output_cost = best_match
return (float(input_tokens) / 1000.0) * input_cost + float(
output_tokens
) / 1000.0 * output_cost
return None
def _extract_messages_truncated(messages: List[Any]) -> List[Dict[str, Any]]:
"""Extract and truncate message content."""
truncated = []
for msg in messages:
if isinstance(msg, dict):
truncated_msg = {"role": msg.get("role", "unknown")}
content = msg.get("content")
if content is not None:
if isinstance(content, list):
# Anthropic supports content as list of blocks
truncated_msg["content"] = _truncate_data(content)
else:
truncated_msg["content"] = _truncate_data(str(content))
truncated.append(truncated_msg)
else:
# Handle message objects
role = getattr(msg, "role", "unknown")
content = getattr(msg, "content", "")
truncated.append({"role": role, "content": _truncate_data(str(content))})
return truncated
def _extract_content_from_response(response: Any) -> Optional[str]:
"""Extract text content from Anthropic response.
Anthropic responses have a ``content`` array with blocks of type
``text`` or ``tool_use``.
"""
if hasattr(response, "content") and response.content:
text_parts = []
for block in response.content:
if hasattr(block, "type") and block.type == "text":
text_parts.append(getattr(block, "text", ""))
elif isinstance(block, dict) and block.get("type") == "text":
text_parts.append(block.get("text", ""))
if text_parts:
return _truncate_data(" ".join(text_parts))
return None
def _extract_tool_calls_from_response(response: Any) -> List[Dict[str, Any]]:
"""Extract tool_use blocks from Anthropic response.
Anthropic tool calls appear as content blocks with ``type: "tool_use"``,
containing ``name`` and ``input`` fields.
"""
tool_calls: List[Dict[str, Any]] = []
if hasattr(response, "content") and response.content:
for block in response.content:
block_type = getattr(block, "type", None) or (
block.get("type") if isinstance(block, dict) else None
)
if block_type == "tool_use":
if isinstance(block, dict):
name = block.get("name", "unknown")
arguments = block.get("input", {})
else:
name = getattr(block, "name", "unknown")
arguments = getattr(block, "input", {})
tool_calls.append({"name": name, "arguments": arguments})
return tool_calls
class _StreamWrapper:
"""Wrapper for Anthropic stream responses to collect events and finalize span."""
def __init__(self, original_stream: Any, trace_ctx: Optional[TraceContext]):
self._original_stream = original_stream
self._trace_ctx = trace_ctx
self._events: List[Any] = []
self._start_time = time.time()
self._model: Optional[str] = None
self._temperature: Optional[float] = None
self._max_tokens: Optional[int] = None
self._messages: Optional[List[Any]] = None
self._parent_span_id = get_current_span_id()
# Accumulated response data from stream events
self._text_content: str = ""
self._tool_calls: List[Dict[str, Any]] = []
self._current_tool: Optional[Dict[str, Any]] = None
self._input_tokens: Optional[int] = None
self._output_tokens: Optional[int] = None
self._response_model: Optional[str] = None
self._stop_reason: Optional[str] = None
def set_params(
self,
model: str,
temperature: Optional[float],
max_tokens: Optional[int],
messages: List[Any],
) -> None:
self._model = model
self._temperature = temperature
self._max_tokens = max_tokens
self._messages = messages
def _process_event(self, event: Any) -> None:
"""Process a single stream event to accumulate response data."""
event_type = getattr(event, "type", None)
if event_type == "message_start":
message = getattr(event, "message", None)
if message:
self._response_model = getattr(message, "model", None)
usage = getattr(message, "usage", None)
if usage:
self._input_tokens = getattr(usage, "input_tokens", None)
elif event_type == "content_block_start":
block = getattr(event, "content_block", None)
if block:
block_type = getattr(block, "type", None)
if block_type == "tool_use":
self._current_tool = {
"name": getattr(block, "name", "unknown"),
"arguments": "",
}
elif event_type == "content_block_delta":
delta = getattr(event, "delta", None)
if delta:
delta_type = getattr(delta, "type", None)
if delta_type == "text_delta":
self._text_content += getattr(delta, "text", "")
elif delta_type == "input_json_delta":
if self._current_tool is not None:
self._current_tool["arguments"] += getattr(
delta, "partial_json", ""
)
elif event_type == "content_block_stop":
if self._current_tool is not None:
# Parse accumulated JSON arguments
try:
args_str = self._current_tool["arguments"]
if isinstance(args_str, str) and args_str:
self._current_tool["arguments"] = json.loads(args_str)
elif not args_str:
self._current_tool["arguments"] = {}
except (json.JSONDecodeError, TypeError):
pass
self._tool_calls.append(self._current_tool)
self._current_tool = None
elif event_type == "message_delta":
delta = getattr(event, "delta", None)
if delta:
self._stop_reason = getattr(delta, "stop_reason", None)
usage = getattr(event, "usage", None)
if usage:
self._output_tokens = getattr(usage, "output_tokens", None)
def __iter__(self) -> Iterator[Any]:
return self
def __next__(self) -> Any:
event = next(self._original_stream)
self._events.append(event)
self._process_event(event)
return event
def __enter__(self) -> "_StreamWrapper":
"""Support context manager protocol for Anthropic streaming."""
if hasattr(self._original_stream, "__enter__"):
self._original_stream.__enter__()
return self
def __exit__(
self,
exc_type: Optional[type],
exc_val: Optional[BaseException],
exc_tb: Optional[Any],
) -> None:
"""Finalize span and close underlying stream on context manager exit."""
if hasattr(self._original_stream, "__exit__"):
self._original_stream.__exit__(exc_type, exc_val, exc_tb)
self.finalize()
def finalize(self) -> None:
"""Create span after stream is fully consumed."""
if not self._events:
return
response_model = self._response_model or self._model or "unknown"
# Build a mock response object for _create_llm_span
mock = _MockResponse()
mock.model = response_model
mock.text_content = self._text_content or None
mock.tool_calls = self._tool_calls
mock.stop_reason = self._stop_reason
mock.input_tokens = self._input_tokens
mock.output_tokens = self._output_tokens
_create_llm_span(
response=mock,
start_time=self._start_time,
model=self._model or response_model,
temperature=self._temperature,
max_tokens=self._max_tokens,
messages=self._messages or [],
parent_span_id=self._parent_span_id,
trace_ctx=self._trace_ctx,
)
if self._trace_ctx:
self._trace_ctx.__exit__(None, None, None)
class _MockResponse:
"""Lightweight object to unify stream-assembled and regular responses."""
def __init__(self) -> None:
self.model: str = "unknown"
self.text_content: Optional[str] = None
self.tool_calls: List[Dict[str, Any]] = []
self.stop_reason: Optional[str] = None
self.input_tokens: Optional[int] = None
self.output_tokens: Optional[int] = None
# Fake content list for compatibility with extraction helpers
self.content: List[Any] = []
def _create_llm_span(
response: Any,
start_time: float,
model: str,
temperature: Optional[float],
max_tokens: Optional[int],
messages: List[Any],
parent_span_id: Optional[str],
trace_ctx: Optional[TraceContext],
) -> None:
"""Create LLM span from Anthropic response."""
from agentlens.models import Span, SpanStatus, SpanType
current_trace = get_current_trace()
if current_trace is None:
logger.warning("No active trace, skipping span creation")
return
end_time = time.time()
duration_ms = int((end_time - start_time) * 1000)
# Extract token usage
token_count = None
cost_usd = None
# Handle real Anthropic response
input_tokens = getattr(response, "input_tokens", None)
output_tokens = getattr(response, "output_tokens", None)
# Real responses have usage object
if input_tokens is None and hasattr(response, "usage"):
usage = response.usage
input_tokens = getattr(usage, "input_tokens", None)
output_tokens = getattr(usage, "output_tokens", None)
if input_tokens is not None and output_tokens is not None:
token_count = input_tokens + output_tokens
cost_usd = _calculate_cost(model, input_tokens, output_tokens)
# Extract content - try helpers first, fall back to mock fields
content = _extract_content_from_response(response)
if content is None:
text_content = getattr(response, "text_content", None)
if text_content:
content = _truncate_data(str(text_content))
# Extract tool calls - try helpers first, fall back to mock fields
tool_calls = _extract_tool_calls_from_response(response)
if not tool_calls:
tool_calls = getattr(response, "tool_calls", []) or []
# Extract stop reason
stop_reason = getattr(response, "stop_reason", None)
# Create span
span_name = f"anthropic.{model}"
span = Span(
name=span_name,
type=SpanType.LLM_CALL.value,
parent_span_id=parent_span_id,
input_data={"messages": _extract_messages_truncated(messages)},
output_data={"content": content, "tool_calls": tool_calls or None},
token_count=token_count,
cost_usd=cost_usd,
duration_ms=duration_ms,
status=SpanStatus.COMPLETED.value,
started_at=_now_iso(),
ended_at=_now_iso(),
metadata={
"model": model,
"temperature": temperature,
"max_tokens": max_tokens,
"stop_reason": stop_reason,
},
)
current_trace.spans.append(span)
# Push onto context stack for decision logging
stack = _get_context_stack()
stack.append(span)
# Log tool call decisions
if tool_calls:
from agentlens.decision import log_decision
# Try to get reasoning from the assistant's text content
reasoning = None
if content:
reasoning = _truncate_data(str(content))
# Build context snapshot
context_snapshot = None
if input_tokens is not None or output_tokens is not None:
context_snapshot = {
"model": model,
"input_tokens": input_tokens,
"output_tokens": output_tokens,
}
for tool_call in tool_calls:
log_decision(
type="TOOL_SELECTION",
chosen={
"name": tool_call.get("name", "unknown"),
"arguments": tool_call.get("arguments", {}),
},
alternatives=[],
reasoning=reasoning,
context_snapshot=context_snapshot,
)
# Always pop from context stack
if stack and stack[-1] == span:
stack.pop()
elif stack and isinstance(stack[-1], Span) and stack[-1].id == span.id:
stack.pop()
def _handle_error(
error: Exception,
start_time: float,
model: str,
temperature: Optional[float],
max_tokens: Optional[int],
messages: List[Any],
parent_span_id: Optional[str],
trace_ctx: Optional[TraceContext],
) -> None:
"""Handle error by creating error span and event."""
from agentlens.models import Span, SpanStatus, SpanType
current_trace = get_current_trace()
if current_trace is None:
return
end_time = time.time()
duration_ms = int((end_time - start_time) * 1000)
# Create error span
span_name = f"anthropic.{model}"
span = Span(
name=span_name,
type=SpanType.LLM_CALL.value,
parent_span_id=parent_span_id,
input_data={"messages": _extract_messages_truncated(messages)},
status=SpanStatus.ERROR.value,
status_message=str(error),
started_at=_now_iso(),
ended_at=_now_iso(),
duration_ms=duration_ms,
metadata={
"model": model,
"temperature": temperature,
"max_tokens": max_tokens,
},
)
current_trace.spans.append(span)
# Create error event
error_event = Event(
type=EventType.ERROR.value,
name=f"{span_name}: {str(error)}",
span_id=span.id,
metadata={"error_type": type(error).__name__},
)
current_trace.events.append(error_event)
# Pop from context stack if needed
stack = _get_context_stack()
if stack and isinstance(stack[-1], Span) and stack[-1].id == span.id:
stack.pop()
def _wrap_create(original_create: Any, is_async: bool = False) -> Any:
"""Wrap Anthropic messages.create method."""
if is_async:
@wraps(original_create)
async def async_traced_create(*args: Any, **kwargs: Any) -> Any:
# Extract parameters
model = kwargs.get("model", "claude-3-5-sonnet-20241022")
temperature = kwargs.get("temperature")
max_tokens = kwargs.get("max_tokens")
messages = kwargs.get("messages", [])
stream = kwargs.get("stream", False)
parent_span_id = get_current_span_id()
start_time = time.time()
# Handle streaming
if stream:
trace_ctx = None
if get_current_trace() is None:
trace_ctx = TraceContext(name=f"anthropic-{model}")
trace_ctx.__enter__()
try:
original_stream = await original_create(*args, **kwargs)
wrapper = _StreamWrapper(original_stream, trace_ctx)
wrapper.set_params(model, temperature, max_tokens, messages)
return wrapper
except Exception as e:
if trace_ctx:
trace_ctx.__exit__(type(e), e, None)
raise
# Non-streaming
trace_ctx = None
if get_current_trace() is None:
trace_ctx = TraceContext(name=f"anthropic-{model}")
trace_ctx.__enter__()
try:
response = await original_create(*args, **kwargs)
_create_llm_span(
response=response,
start_time=start_time,
model=model,
temperature=temperature,
max_tokens=max_tokens,
messages=messages,
parent_span_id=parent_span_id,
trace_ctx=trace_ctx,
)
if trace_ctx is not None:
trace_ctx.__exit__(None, None, None)
return response
except Exception as e:
_handle_error(
error=e,
start_time=start_time,
model=model,
temperature=temperature,
max_tokens=max_tokens,
messages=messages,
parent_span_id=parent_span_id,
trace_ctx=trace_ctx,
)
raise
return async_traced_create
else:
@wraps(original_create)
def traced_create(*args: Any, **kwargs: Any) -> Any:
# Extract parameters
model = kwargs.get("model", "claude-3-5-sonnet-20241022")
temperature = kwargs.get("temperature")
max_tokens = kwargs.get("max_tokens")
messages = kwargs.get("messages", [])
stream = kwargs.get("stream", False)
parent_span_id = get_current_span_id()
start_time = time.time()
# Handle streaming
if stream:
trace_ctx = None
if get_current_trace() is None:
trace_ctx = TraceContext(name=f"anthropic-{model}")
trace_ctx.__enter__()
try:
original_stream = original_create(*args, **kwargs)
wrapper = _StreamWrapper(original_stream, trace_ctx)
wrapper.set_params(model, temperature, max_tokens, messages)
return wrapper
except Exception as e:
if trace_ctx:
trace_ctx.__exit__(type(e), e, None)
raise
# Non-streaming
trace_ctx = None
if get_current_trace() is None:
trace_ctx = TraceContext(name=f"anthropic-{model}")
trace_ctx.__enter__()
try:
response = original_create(*args, **kwargs)
_create_llm_span(
response=response,
start_time=start_time,
model=model,
temperature=temperature,
max_tokens=max_tokens,
messages=messages,
parent_span_id=parent_span_id,
trace_ctx=trace_ctx,
)
if trace_ctx is not None:
trace_ctx.__exit__(None, None, None)
return response
except Exception as e:
_handle_error(
error=e,
start_time=start_time,
model=model,
temperature=temperature,
max_tokens=max_tokens,
messages=messages,
parent_span_id=parent_span_id,
trace_ctx=trace_ctx,
)
raise
return traced_create
def wrap_anthropic(client: Any) -> Any:
"""Wrap an Anthropic client to add AgentLens tracing.
Instruments ``client.messages.create()`` to automatically capture LLM spans,
token usage, cost estimation, and tool-call decisions.
Supports both sync (``anthropic.Anthropic``) and async
(``anthropic.AsyncAnthropic``) clients as well as streaming responses.
Args:
client: An ``anthropic.Anthropic`` or ``anthropic.AsyncAnthropic`` instance.
Returns:
The same client instance with ``messages.create`` wrapped for tracing.
Example::
import anthropic
from agentlens.integrations.anthropic import wrap_anthropic
client = anthropic.Anthropic(api_key="sk-...")
traced_client = wrap_anthropic(client)
response = traced_client.messages.create(
model="claude-3-sonnet-20240229",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}]
)
"""
# Detect async client by checking for common async patterns
is_async = False
try:
import asyncio
import inspect
create_method = client.messages.create
if inspect.iscoroutinefunction(create_method) or (
hasattr(create_method, "__wrapped__")
and inspect.iscoroutinefunction(create_method.__wrapped__)
):
is_async = True
except (AttributeError, ImportError):
pass
# Also detect by class name as a fallback
client_class_name = type(client).__name__
if "Async" in client_class_name:
is_async = True
original_create = client.messages.create
traced_create = _wrap_create(original_create, is_async=is_async)
client.messages.create = traced_create
logger.debug("Anthropic client wrapped with AgentLens tracing")
return client

View File

@@ -26,16 +26,34 @@ logger = logging.getLogger("agentlens")
# Cost per 1K tokens (input/output) for common models
_MODEL_COSTS: Dict[str, tuple] = {
# GPT-5 family
"gpt-5.2": (0.00175, 0.014),
"gpt-5.1": (0.00125, 0.01),
"gpt-5": (0.00125, 0.01),
"gpt-5-mini": (0.00025, 0.002),
"gpt-5-nano": (0.00005, 0.0004),
# GPT-4.1 family
"gpt-4.1": (0.002, 0.008),
"gpt-4.1-mini": (0.0004, 0.0016),
"gpt-4.1-nano": (0.0001, 0.0004),
# o-series reasoning models
"o3": (0.002, 0.008),
"o3-mini": (0.0011, 0.0044),
"o4-mini": (0.0011, 0.0044),
"o1": (0.015, 0.06),
# GPT-4o family
"gpt-4o": (0.0025, 0.01),
"gpt-4o-2024-05-13": (0.005, 0.015),
"gpt-4o-2024-08-06": (0.0025, 0.01),
"gpt-4o-mini": (0.00015, 0.0006),
"gpt-4o-mini-2024-07-18": (0.00015, 0.0006),
# GPT-4 family
"gpt-4": (0.03, 0.06),
"gpt-4-32k": (0.06, 0.12),
"gpt-4-turbo": (0.01, 0.03),
"gpt-4-turbo-2024-04-09": (0.01, 0.03),
"gpt-4-turbo-preview": (0.01, 0.03),
"gpt-4o": (0.005, 0.015),
"gpt-4o-2024-05-13": (0.005, 0.015),
"gpt-4o-2024-08-06": (0.0025, 0.01),
"gpt-4o-mini": (0.00015, 0.0006),
"gpt-4o-mini-2024-07-18": (0.00015, 0.0006),
# GPT-3.5 family
"gpt-3.5-turbo": (0.0005, 0.0015),
"gpt-3.5-turbo-0125": (0.0005, 0.0015),
"gpt-3.5-turbo-1106": (0.001, 0.002),

124
packages/sdk-ts/README.md Normal file
View File

@@ -0,0 +1,124 @@
# agentlens-sdk
TypeScript SDK for AgentLens — Agent observability that traces decisions, not just API calls.
[![npm version](https://img.shields.io/npm/v/agentlens-sdk.svg)](https://www.npmjs.com/package/agentlens-sdk)
[![license](https://img.shields.io/npm/l/agentlens-sdk.svg)](https://github.com/repi/agentlens/blob/main/LICENSE)
## Install
```bash
npm install agentlens-sdk
```
## Quick Start
```typescript
import { init, TraceBuilder, shutdown } from "agentlens-sdk";
// Initialize the SDK
init({
apiKey: "your-api-key",
endpoint: "https://agentlens.vectry.tech/api",
});
// Create a trace
const trace = new TraceBuilder("agent-run-123", "My Agent Task");
// Add a span (tool call, LLM call, etc.)
trace.addSpan({
name: "search-documents",
type: "tool",
input: { query: "quarterly report" },
output: { results: 5 },
});
// Record a decision point
trace.addDecision({
name: "select-tool",
type: "tool_selection",
options: ["search", "calculate", "summarize"],
selected: "search",
reasoning: "User asked for document lookup",
});
// Finalize and send
await trace.end();
// Flush remaining data before exit
await shutdown();
```
## API Reference
### Core Functions
| Function | Description |
|---|---|
| `init(options)` | Initialize the SDK with your API key and configuration. |
| `shutdown()` | Flush pending data and shut down the transport. |
| `flush()` | Manually flush the current batch without shutting down. |
| `getClient()` | Return the initialized client instance. |
### TraceBuilder
The primary interface for constructing traces.
```typescript
const trace = new TraceBuilder(traceId: string, name: string);
trace.addSpan(span: SpanPayload); // Add a span to the trace
trace.addDecision(decision: DecisionPointPayload); // Record a decision point
trace.end(): Promise<void>; // Finalize and send the trace
```
### Standalone Helpers
| Function | Description |
|---|---|
| `createDecision(decision)` | Create and send a standalone decision point outside a trace. |
## Types
Core payload types used throughout the SDK:
- **`TracePayload`** — Top-level trace structure containing spans and metadata.
- **`SpanPayload`** — Individual unit of work (tool call, LLM request, retrieval, etc.).
- **`DecisionPointPayload`** — A recorded decision: what options existed, what was chosen, and why.
- **`EventPayload`** — Discrete event within a span or trace.
- **`JsonValue`** — Flexible JSON-compatible value type for inputs/outputs.
## Enums
| Enum | Values |
|---|---|
| `TraceStatus` | Status of the overall trace (e.g., running, completed, failed). |
| `SpanType` | Category of span (e.g., tool, llm, retrieval). |
| `SpanStatus` | Status of an individual span. |
| `DecisionType` | Category of decision (e.g., tool_selection, routing). |
| `EventType` | Category of event within a span. |
## Configuration
Pass `InitOptions` to `init()`:
```typescript
init({
apiKey: "your-api-key", // Required. Your AgentLens API key.
endpoint: "https://...", // API endpoint. Defaults to AgentLens cloud.
maxBatchSize: 100, // Max items per batch before auto-flush.
flushInterval: 5000, // Auto-flush interval in milliseconds.
});
```
## Transport
The SDK ships with `BatchTransport`, which batches payloads and flushes them on an interval or when the batch size threshold is reached. This is used internally by `init()` — you typically do not need to instantiate it directly.
## Documentation
Full documentation: [agentlens.vectry.tech/docs/typescript-sdk](https://agentlens.vectry.tech/docs/typescript-sdk)
## License
MIT

View File

@@ -0,0 +1,56 @@
{
"name": "agentlens-sdk",
"version": "0.1.0",
"description": "AgentLens TypeScript SDK — Agent observability that traces decisions, not just API calls.",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
},
"engines": {
"node": ">=20"
},
"license": "MIT",
"author": "Vectry <hunter@repi.fun>",
"repository": {
"type": "git",
"url": "https://gitea.repi.fun/repi/agentlens",
"directory": "packages/sdk-ts"
},
"homepage": "https://agentlens.vectry.tech",
"bugs": {
"url": "https://gitea.repi.fun/repi/agentlens/issues"
},
"keywords": [
"agentlens",
"observability",
"tracing",
"ai-agents",
"sdk",
"typescript"
],
"devDependencies": {
"tsup": "^8.3.0",
"typescript": "^5.7.0"
}
}

View File

@@ -0,0 +1,11 @@
import type { BatchTransport } from "./transport.js";
let _transport: BatchTransport | null = null;
export function _setTransport(transport: BatchTransport | null): void {
_transport = transport;
}
export function _getTransport(): BatchTransport | null {
return _transport;
}

View File

@@ -0,0 +1,29 @@
import type { DecisionPointPayload, DecisionType, JsonValue } from "./models.js";
import { generateId, nowISO } from "./models.js";
export interface CreateDecisionInput {
type: DecisionType;
chosen: JsonValue;
alternatives?: JsonValue[];
reasoning?: string;
contextSnapshot?: JsonValue;
durationMs?: number;
costUsd?: number;
parentSpanId?: string;
timestamp?: string;
}
export function createDecision(input: CreateDecisionInput): DecisionPointPayload {
return {
id: generateId(),
type: input.type,
chosen: input.chosen,
alternatives: input.alternatives ?? [],
reasoning: input.reasoning,
contextSnapshot: input.contextSnapshot,
durationMs: input.durationMs,
costUsd: input.costUsd,
parentSpanId: input.parentSpanId,
timestamp: input.timestamp ?? nowISO(),
};
}

View File

@@ -0,0 +1,76 @@
import { BatchTransport } from "./transport.js";
import { _setTransport, _getTransport } from "./_registry.js";
export interface InitOptions {
apiKey: string;
endpoint?: string;
maxBatchSize?: number;
flushInterval?: number;
}
export function init(options: InitOptions): void {
const existing = _getTransport();
if (existing) {
void existing.shutdown();
}
_setTransport(
new BatchTransport({
apiKey: options.apiKey,
endpoint: options.endpoint ?? "https://agentlens.vectry.tech",
maxBatchSize: options.maxBatchSize,
flushInterval: options.flushInterval,
}),
);
}
export async function shutdown(): Promise<void> {
const transport = _getTransport();
if (transport) {
await transport.shutdown();
_setTransport(null);
}
}
export function getClient(): BatchTransport | null {
return _getTransport();
}
export async function flush(): Promise<void> {
const transport = _getTransport();
if (transport) {
await transport.flush();
}
}
export {
TraceStatus,
DecisionType,
SpanType,
SpanStatus,
EventType,
generateId,
nowISO,
} from "./models.js";
export type {
JsonValue,
DecisionPointPayload,
SpanPayload,
EventPayload,
TracePayload,
} from "./models.js";
export { BatchTransport } from "./transport.js";
export type { BatchTransportOptions } from "./transport.js";
export { TraceBuilder } from "./trace.js";
export type {
TraceBuilderOptions,
AddSpanInput,
AddDecisionInput,
AddEventInput,
EndOptions,
} from "./trace.js";
export { createDecision } from "./decision.js";
export type { CreateDecisionInput } from "./decision.js";

View File

@@ -0,0 +1,136 @@
import { randomUUID } from "crypto";
// ---------------------------------------------------------------------------
// JSON value type (replaces Prisma.JsonValue for the SDK)
// ---------------------------------------------------------------------------
export type JsonValue =
| string
| number
| boolean
| null
| JsonValue[]
| { [key: string]: JsonValue };
// ---------------------------------------------------------------------------
// Enums (as const + type union pattern — NO TypeScript enum keyword)
// ---------------------------------------------------------------------------
export const TraceStatus = {
RUNNING: "RUNNING",
COMPLETED: "COMPLETED",
ERROR: "ERROR",
} as const;
export type TraceStatus = (typeof TraceStatus)[keyof typeof TraceStatus];
export const DecisionType = {
TOOL_SELECTION: "TOOL_SELECTION",
ROUTING: "ROUTING",
RETRY: "RETRY",
ESCALATION: "ESCALATION",
MEMORY_RETRIEVAL: "MEMORY_RETRIEVAL",
PLANNING: "PLANNING",
CUSTOM: "CUSTOM",
} as const;
export type DecisionType = (typeof DecisionType)[keyof typeof DecisionType];
export const SpanType = {
LLM_CALL: "LLM_CALL",
TOOL_CALL: "TOOL_CALL",
MEMORY_OP: "MEMORY_OP",
CHAIN: "CHAIN",
AGENT: "AGENT",
CUSTOM: "CUSTOM",
} as const;
export type SpanType = (typeof SpanType)[keyof typeof SpanType];
export const SpanStatus = {
RUNNING: "RUNNING",
COMPLETED: "COMPLETED",
ERROR: "ERROR",
} as const;
export type SpanStatus = (typeof SpanStatus)[keyof typeof SpanStatus];
export const EventType = {
ERROR: "ERROR",
RETRY: "RETRY",
FALLBACK: "FALLBACK",
CONTEXT_OVERFLOW: "CONTEXT_OVERFLOW",
USER_FEEDBACK: "USER_FEEDBACK",
CUSTOM: "CUSTOM",
} as const;
export type EventType = (typeof EventType)[keyof typeof EventType];
// ---------------------------------------------------------------------------
// Wire-format interfaces (camelCase, matching POST /api/traces contract)
// ---------------------------------------------------------------------------
export interface DecisionPointPayload {
id: string;
type: DecisionType;
chosen: JsonValue;
alternatives: JsonValue[];
reasoning?: string;
contextSnapshot?: JsonValue;
durationMs?: number;
costUsd?: number;
parentSpanId?: string;
timestamp: string;
}
export interface SpanPayload {
id: string;
parentSpanId?: string;
name: string;
type: SpanType;
input?: JsonValue;
output?: JsonValue;
tokenCount?: number;
costUsd?: number;
durationMs?: number;
status: SpanStatus;
statusMessage?: string;
startedAt: string;
endedAt?: string;
metadata?: JsonValue;
}
export interface EventPayload {
id: string;
spanId?: string;
type: EventType;
name: string;
metadata?: JsonValue;
timestamp: string;
}
export interface TracePayload {
id: string;
name: string;
sessionId?: string;
status: TraceStatus;
tags: string[];
metadata?: JsonValue;
totalCost?: number;
totalTokens?: number;
totalDuration?: number;
startedAt: string;
endedAt?: string;
decisionPoints: DecisionPointPayload[];
spans: SpanPayload[];
events: EventPayload[];
}
// ---------------------------------------------------------------------------
// Helper functions
// ---------------------------------------------------------------------------
/** Generate a v4 UUID. */
export function generateId(): string {
return randomUUID();
}
/** Return the current time as an ISO-8601 string. */
export function nowISO(): string {
return new Date().toISOString();
}

View File

@@ -0,0 +1,185 @@
import type {
TracePayload,
SpanPayload,
DecisionPointPayload,
EventPayload,
JsonValue,
TraceStatus,
SpanType,
SpanStatus,
DecisionType,
EventType,
} from "./models.js";
import {
generateId,
nowISO,
TraceStatus as TraceStatusValues,
SpanStatus as SpanStatusValues,
} from "./models.js";
import { _getTransport } from "./_registry.js";
export interface TraceBuilderOptions {
sessionId?: string;
tags?: string[];
metadata?: JsonValue;
}
export interface AddSpanInput {
id?: string;
parentSpanId?: string;
name: string;
type: SpanType;
input?: JsonValue;
output?: JsonValue;
tokenCount?: number;
costUsd?: number;
durationMs?: number;
status?: SpanStatus;
statusMessage?: string;
startedAt?: string;
endedAt?: string;
metadata?: JsonValue;
}
export interface AddDecisionInput {
id?: string;
type: DecisionType;
chosen: JsonValue;
alternatives?: JsonValue[];
reasoning?: string;
contextSnapshot?: JsonValue;
durationMs?: number;
costUsd?: number;
parentSpanId?: string;
timestamp?: string;
}
export interface AddEventInput {
id?: string;
spanId?: string;
type: EventType;
name: string;
metadata?: JsonValue;
timestamp?: string;
}
export interface EndOptions {
status?: TraceStatus;
metadata?: JsonValue;
totalCost?: number;
totalTokens?: number;
}
export class TraceBuilder {
private readonly trace: TracePayload;
private readonly startMs: number;
constructor(name: string, options?: TraceBuilderOptions) {
this.startMs = Date.now();
this.trace = {
id: generateId(),
name,
sessionId: options?.sessionId,
status: TraceStatusValues.RUNNING,
tags: options?.tags ?? [],
metadata: options?.metadata,
startedAt: nowISO(),
decisionPoints: [],
spans: [],
events: [],
};
}
addSpan(input: AddSpanInput): string {
const id = input.id ?? generateId();
const span: SpanPayload = {
id,
parentSpanId: input.parentSpanId,
name: input.name,
type: input.type,
input: input.input,
output: input.output,
tokenCount: input.tokenCount,
costUsd: input.costUsd,
durationMs: input.durationMs,
status: input.status ?? SpanStatusValues.RUNNING,
statusMessage: input.statusMessage,
startedAt: input.startedAt ?? nowISO(),
endedAt: input.endedAt,
metadata: input.metadata,
};
this.trace.spans.push(span);
return id;
}
addDecision(input: AddDecisionInput): string {
const id = input.id ?? generateId();
const decision: DecisionPointPayload = {
id,
type: input.type,
chosen: input.chosen,
alternatives: input.alternatives ?? [],
reasoning: input.reasoning,
contextSnapshot: input.contextSnapshot,
durationMs: input.durationMs,
costUsd: input.costUsd,
parentSpanId: input.parentSpanId,
timestamp: input.timestamp ?? nowISO(),
};
this.trace.decisionPoints.push(decision);
return id;
}
addEvent(input: AddEventInput): string {
const id = input.id ?? generateId();
const event: EventPayload = {
id,
spanId: input.spanId,
type: input.type,
name: input.name,
metadata: input.metadata,
timestamp: input.timestamp ?? nowISO(),
};
this.trace.events.push(event);
return id;
}
setStatus(status: TraceStatus): this {
this.trace.status = status;
return this;
}
setMetadata(metadata: JsonValue): this {
this.trace.metadata = metadata;
return this;
}
toPayload(): TracePayload {
return { ...this.trace };
}
end(options?: EndOptions): TracePayload {
const endedAt = nowISO();
this.trace.endedAt = endedAt;
this.trace.totalDuration = Date.now() - this.startMs;
this.trace.status =
options?.status ?? TraceStatusValues.COMPLETED;
if (options?.metadata !== undefined) {
this.trace.metadata = options.metadata;
}
if (options?.totalCost !== undefined) {
this.trace.totalCost = options.totalCost;
}
if (options?.totalTokens !== undefined) {
this.trace.totalTokens = options.totalTokens;
}
const transport = _getTransport();
if (transport) {
transport.add(this.trace);
}
return { ...this.trace };
}
}

View File

@@ -0,0 +1,77 @@
import type { TracePayload } from "./models.js";
export interface BatchTransportOptions {
apiKey: string;
endpoint: string;
maxBatchSize?: number;
flushInterval?: number;
}
export class BatchTransport {
private readonly apiKey: string;
private readonly endpoint: string;
private readonly maxBatchSize: number;
private readonly flushInterval: number;
private buffer: TracePayload[] = [];
private timer: ReturnType<typeof setInterval> | null = null;
constructor(options: BatchTransportOptions) {
this.apiKey = options.apiKey;
this.endpoint = options.endpoint.replace(/\/+$/, "");
this.maxBatchSize = options.maxBatchSize ?? 10;
this.flushInterval = options.flushInterval ?? 5_000;
this.timer = setInterval(() => {
void this._doFlush();
}, this.flushInterval);
}
add(trace: TracePayload): void {
this.buffer.push(trace);
if (this.buffer.length >= this.maxBatchSize) {
void this._doFlush();
}
}
async flush(): Promise<void> {
await this._doFlush();
}
async shutdown(): Promise<void> {
if (this.timer !== null) {
clearInterval(this.timer);
this.timer = null;
}
await this._doFlush();
}
private async _doFlush(): Promise<void> {
if (this.buffer.length === 0) {
return;
}
const batch = this.buffer.splice(0, this.buffer.length);
try {
const response = await fetch(`${this.endpoint}/api/traces`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ traces: batch }),
});
if (!response.ok) {
const text = await response.text().catch(() => "");
console.warn(
`AgentLens: Failed to send traces (HTTP ${response.status}): ${text.slice(0, 200)}`,
);
}
} catch (error: unknown) {
const message =
error instanceof Error ? error.message : String(error);
console.warn(`AgentLens: Failed to send traces: ${message}`);
}
}
}

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,9 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm", "cjs"],
dts: true,
clean: true,
sourcemap: true,
});