Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b388484f8 | ||
|
|
42b5379ce1 | ||
|
|
f0ce0f7884 | ||
|
|
434e68991d | ||
|
|
5256bf005b | ||
|
|
6bed493275 | ||
|
|
0149e0a6f4 | ||
|
|
4f7719eace | ||
|
|
92b98f2d6f | ||
|
|
145b1669e7 | ||
|
|
d91fdfc81a |
@@ -6,6 +6,8 @@ FROM base AS deps
|
|||||||
COPY package.json package-lock.json* ./
|
COPY package.json package-lock.json* ./
|
||||||
COPY apps/web/package.json ./apps/web/
|
COPY apps/web/package.json ./apps/web/
|
||||||
COPY packages/database/package.json ./packages/database/
|
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
|
RUN npm install --production=false
|
||||||
|
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
@@ -19,6 +21,7 @@ RUN addgroup --system --gid 1001 nodejs && \
|
|||||||
adduser --system --uid 1001 nextjs
|
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/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/.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
|
COPY --from=builder --chown=nextjs:nodejs /app/packages/database/prisma ./packages/database/prisma
|
||||||
USER nextjs
|
USER nextjs
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"next": "^15.1.0",
|
"next": "^15.1.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0",
|
||||||
|
"shiki": "^3.22.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.0.0",
|
"@tailwindcss/postcss": "^4.0.0",
|
||||||
|
|||||||
69
apps/web/public/llms.txt
Normal file
69
apps/web/public/llms.txt
Normal 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
|
||||||
127
apps/web/src/app/api/decisions/route.ts
Normal file
127
apps/web/src/app/api/decisions/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
apps/web/src/app/api/settings/purge/route.ts
Normal file
21
apps/web/src/app/api/settings/purge/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
apps/web/src/app/api/settings/stats/route.ts
Normal file
25
apps/web/src/app/api/settings/stats/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,26 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { prisma } from "@/lib/prisma";
|
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
|
// GET /api/traces/[id] — Get single trace with all relations
|
||||||
export async function GET(
|
export async function GET(
|
||||||
_request: Request,
|
_request: NextRequest,
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: RouteParams
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
@@ -41,11 +57,13 @@ export async function GET(
|
|||||||
// Transform data to match frontend expectations
|
// Transform data to match frontend expectations
|
||||||
const transformedTrace = {
|
const transformedTrace = {
|
||||||
...trace,
|
...trace,
|
||||||
|
durationMs: trace.totalDuration,
|
||||||
|
costUsd: trace.totalCost,
|
||||||
decisionPoints: trace.decisionPoints.map((dp) => ({
|
decisionPoints: trace.decisionPoints.map((dp) => ({
|
||||||
id: dp.id,
|
id: dp.id,
|
||||||
type: dp.type,
|
type: dp.type,
|
||||||
chosenAction: typeof dp.chosen === "string" ? dp.chosen : JSON.stringify(dp.chosen),
|
chosenAction: extractActionLabel(dp.chosen),
|
||||||
alternatives: dp.alternatives.map((alt) => (typeof alt === "string" ? alt : JSON.stringify(alt))),
|
alternatives: dp.alternatives.map((alt) => extractActionLabel(alt)),
|
||||||
reasoning: dp.reasoning,
|
reasoning: dp.reasoning,
|
||||||
contextSnapshot: dp.contextSnapshot as Record<string, unknown> | null,
|
contextSnapshot: dp.contextSnapshot as Record<string, unknown> | null,
|
||||||
confidence: null, // Not in schema, default to 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 });
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,6 +63,24 @@ interface BatchTracesRequest {
|
|||||||
traces: TracePayload[];
|
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
|
// POST /api/traces — Batch ingest traces from SDK
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -166,10 +184,15 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert traces using transaction
|
// Insert traces using interactive transaction to control insert order.
|
||||||
const result = await prisma.$transaction(
|
// Spans must be inserted before decision points due to the
|
||||||
body.traces.map((trace) =>
|
// DecisionPoint.parentSpanId FK referencing Span.id.
|
||||||
prisma.trace.create({
|
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: {
|
data: {
|
||||||
id: trace.id,
|
id: trace.id,
|
||||||
name: trace.name,
|
name: trace.name,
|
||||||
@@ -182,23 +205,18 @@ export async function POST(request: NextRequest) {
|
|||||||
totalDuration: trace.totalDuration,
|
totalDuration: trace.totalDuration,
|
||||||
startedAt: new Date(trace.startedAt),
|
startedAt: new Date(trace.startedAt),
|
||||||
endedAt: trace.endedAt ? new Date(trace.endedAt) : null,
|
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,
|
id: span.id,
|
||||||
|
traceId: trace.id,
|
||||||
parentSpanId: span.parentSpanId,
|
parentSpanId: span.parentSpanId,
|
||||||
name: span.name,
|
name: span.name,
|
||||||
type: span.type,
|
type: span.type,
|
||||||
@@ -212,22 +230,53 @@ export async function POST(request: NextRequest) {
|
|||||||
startedAt: new Date(span.startedAt),
|
startedAt: new Date(span.startedAt),
|
||||||
endedAt: span.endedAt ? new Date(span.endedAt) : null,
|
endedAt: span.endedAt ? new Date(span.endedAt) : null,
|
||||||
metadata: span.metadata as Prisma.InputJsonValue | undefined,
|
metadata: span.metadata as Prisma.InputJsonValue | undefined,
|
||||||
})),
|
|
||||||
},
|
},
|
||||||
events: {
|
});
|
||||||
create: trace.events.map((event) => ({
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
id: event.id,
|
||||||
|
traceId: trace.id,
|
||||||
spanId: event.spanId,
|
spanId: event.spanId,
|
||||||
type: event.type,
|
type: event.type,
|
||||||
name: event.name,
|
name: event.name,
|
||||||
metadata: event.metadata as Prisma.InputJsonValue | undefined,
|
metadata: event.metadata as Prisma.InputJsonValue | undefined,
|
||||||
timestamp: new Date(event.timestamp),
|
timestamp: new Date(event.timestamp),
|
||||||
})),
|
})),
|
||||||
},
|
});
|
||||||
},
|
}
|
||||||
})
|
|
||||||
)
|
created.push(trace.id);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
return created;
|
||||||
|
});
|
||||||
|
|
||||||
return NextResponse.json({ success: true, count: result.length }, { status: 200 });
|
return NextResponse.json({ success: true, count: result.length }, { status: 200 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
402
apps/web/src/app/dashboard/decisions/page.tsx
Normal file
402
apps/web/src/app/dashboard/decisions/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
GitBranch,
|
GitBranch,
|
||||||
Settings,
|
Settings,
|
||||||
Menu,
|
Menu,
|
||||||
X,
|
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -22,8 +21,8 @@ interface NavItem {
|
|||||||
|
|
||||||
const navItems: NavItem[] = [
|
const navItems: NavItem[] = [
|
||||||
{ href: "/dashboard", label: "Traces", icon: Activity },
|
{ href: "/dashboard", label: "Traces", icon: Activity },
|
||||||
{ href: "/dashboard/decisions", label: "Decisions", icon: GitBranch, comingSoon: true },
|
{ href: "/dashboard/decisions", label: "Decisions", icon: GitBranch },
|
||||||
{ href: "/dashboard/settings", label: "Settings", icon: Settings, comingSoon: true },
|
{ href: "/dashboard/settings", label: "Settings", icon: Settings },
|
||||||
];
|
];
|
||||||
|
|
||||||
function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
|
function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
|
||||||
|
|||||||
294
apps/web/src/app/dashboard/settings/page.tsx
Normal file
294
apps/web/src/app/dashboard/settings/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
615
apps/web/src/app/docs/api-reference/page.tsx
Normal file
615
apps/web/src/app/docs/api-reference/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
265
apps/web/src/app/docs/concepts/page.tsx
Normal file
265
apps/web/src/app/docs/concepts/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
121
apps/web/src/app/docs/docs-sidebar.tsx
Normal file
121
apps/web/src/app/docs/docs-sidebar.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
212
apps/web/src/app/docs/getting-started/page.tsx
Normal file
212
apps/web/src/app/docs/getting-started/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
177
apps/web/src/app/docs/integrations/anthropic/page.tsx
Normal file
177
apps/web/src/app/docs/integrations/anthropic/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
199
apps/web/src/app/docs/integrations/langchain/page.tsx
Normal file
199
apps/web/src/app/docs/integrations/langchain/page.tsx
Normal 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'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
188
apps/web/src/app/docs/integrations/openai/page.tsx
Normal file
188
apps/web/src/app/docs/integrations/openai/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
63
apps/web/src/app/docs/layout.tsx
Normal file
63
apps/web/src/app/docs/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
216
apps/web/src/app/docs/opencode-plugin/page.tsx
Normal file
216
apps/web/src/app/docs/opencode-plugin/page.tsx
Normal 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 "false" to disable (default: "true")</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
131
apps/web/src/app/docs/page.tsx
Normal file
131
apps/web/src/app/docs/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
313
apps/web/src/app/docs/python-sdk/page.tsx
Normal file
313
apps/web/src/app/docs/python-sdk/page.tsx
Normal 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 "false" 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
241
apps/web/src/app/docs/self-hosting/page.tsx
Normal file
241
apps/web/src/app/docs/self-hosting/page.tsx
Normal 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 "development" 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
287
apps/web/src/app/docs/typescript-sdk/page.tsx
Normal file
287
apps/web/src/app/docs/typescript-sdk/page.tsx
Normal 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<string, unknown></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?: "COMPLETED" | "ERROR"): Promise<void>
|
||||||
|
</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 "false" to disable</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,8 +5,63 @@ import "./globals.css";
|
|||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "AgentLens",
|
metadataBase: new URL("https://agentlens.vectry.tech"),
|
||||||
description: "Agent observability that traces decisions, not just API calls",
|
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({
|
export default function RootLayout({
|
||||||
|
|||||||
@@ -20,6 +20,46 @@ import {
|
|||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-neutral-950">
|
<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 */}
|
{/* Hero Section */}
|
||||||
<section className="relative overflow-hidden border-b border-neutral-800/50">
|
<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" />
|
<div className="absolute inset-0 bg-gradient-to-b from-emerald-500/5 via-transparent to-transparent" />
|
||||||
|
|||||||
22
apps/web/src/app/robots.ts
Normal file
22
apps/web/src/app/robots.ts
Normal 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",
|
||||||
|
};
|
||||||
|
}
|
||||||
22
apps/web/src/app/sitemap.ts
Normal file
22
apps/web/src/app/sitemap.ts
Normal 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 },
|
||||||
|
];
|
||||||
|
}
|
||||||
34
apps/web/src/components/code-block.tsx
Normal file
34
apps/web/src/components/code-block.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
apps/web/src/components/copy-button.tsx
Normal file
24
apps/web/src/components/copy-button.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -461,13 +461,50 @@ function CostBreakdown({
|
|||||||
// Section C: Token Usage Gauge
|
// Section C: Token Usage Gauge
|
||||||
function TokenUsageGauge({ trace }: { trace: Trace }) {
|
function TokenUsageGauge({ trace }: { trace: Trace }) {
|
||||||
const tokenData = useMemo(() => {
|
const tokenData = useMemo(() => {
|
||||||
// Try to get total tokens from various sources
|
|
||||||
const totalTokens =
|
const totalTokens =
|
||||||
(trace.metadata?.totalTokens as number | null | undefined) ??
|
(trace.metadata?.totalTokens as number | null | undefined) ??
|
||||||
(trace.metadata?.tokenCount as number | null | undefined) ??
|
(trace.metadata?.tokenCount as number | null | undefined) ??
|
||||||
null;
|
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 {
|
return {
|
||||||
totalTokens,
|
totalTokens,
|
||||||
|
|||||||
@@ -18,13 +18,19 @@ import {
|
|||||||
DollarSign,
|
DollarSign,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Terminal,
|
Terminal,
|
||||||
|
Cpu,
|
||||||
|
FileEdit,
|
||||||
|
Search,
|
||||||
|
Eye,
|
||||||
|
FolderOpen,
|
||||||
|
FilePlus,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn, formatDuration, formatRelativeTime } from "@/lib/utils";
|
import { cn, formatDuration, formatRelativeTime } from "@/lib/utils";
|
||||||
import { DecisionTree } from "./decision-tree";
|
import { DecisionTree } from "./decision-tree";
|
||||||
import { TraceAnalytics } from "./trace-analytics";
|
import { TraceAnalytics } from "./trace-analytics";
|
||||||
|
|
||||||
type TraceStatus = "RUNNING" | "COMPLETED" | "ERROR";
|
type TraceStatus = "RUNNING" | "COMPLETED" | "ERROR";
|
||||||
type TabType = "tree" | "analytics" | "decisions" | "spans" | "events";
|
type TabType = "tree" | "analytics" | "decisions" | "spans" | "events" | "agent";
|
||||||
|
|
||||||
interface DecisionPoint {
|
interface DecisionPoint {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -278,6 +284,14 @@ export function TraceDetail({
|
|||||||
icon={Activity}
|
icon={Activity}
|
||||||
label={`Events (${events.length})`}
|
label={`Events (${events.length})`}
|
||||||
/>
|
/>
|
||||||
|
{trace.tags.includes("opencode") && (
|
||||||
|
<TabButton
|
||||||
|
active={activeTab === "agent"}
|
||||||
|
onClick={() => setActiveTab("agent")}
|
||||||
|
icon={Cpu}
|
||||||
|
label="Agent"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -304,6 +318,9 @@ export function TraceDetail({
|
|||||||
)}
|
)}
|
||||||
{activeTab === "spans" && <SpansTab spans={spans} />}
|
{activeTab === "spans" && <SpansTab spans={spans} />}
|
||||||
{activeTab === "events" && <EventsTab events={events} />}
|
{activeTab === "events" && <EventsTab events={events} />}
|
||||||
|
{activeTab === "agent" && (
|
||||||
|
<CodingAgentTab spans={spans} events={events} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -566,3 +583,239 @@ function EventsTab({ events }: { events: Event[] }) {
|
|||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
347
examples/moonshot_real_test.py
Normal file
347
examples/moonshot_real_test.py
Normal 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
1988
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
88
packages/opencode-plugin/README.md
Normal file
88
packages/opencode-plugin/README.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# opencode-agentlens
|
||||||
|
|
||||||
|
OpenCode plugin for AgentLens — trace your coding agent's decisions, tool calls, and sessions.
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/opencode-agentlens)
|
||||||
|
[](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
|
||||||
57
packages/opencode-plugin/package.json
Normal file
57
packages/opencode-plugin/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
41
packages/opencode-plugin/src/config.ts
Normal file
41
packages/opencode-plugin/src/config.ts
Normal 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),
|
||||||
|
};
|
||||||
|
}
|
||||||
146
packages/opencode-plugin/src/index.ts
Normal file
146
packages/opencode-plugin/src/index.ts
Normal 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";
|
||||||
183
packages/opencode-plugin/src/state.ts
Normal file
183
packages/opencode-plugin/src/state.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
116
packages/opencode-plugin/src/utils.ts
Normal file
116
packages/opencode-plugin/src/utils.ts
Normal 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);
|
||||||
|
}
|
||||||
21
packages/opencode-plugin/tsconfig.json
Normal file
21
packages/opencode-plugin/tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
9
packages/opencode-plugin/tsup.config.ts
Normal file
9
packages/opencode-plugin/tsup.config.ts
Normal 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,
|
||||||
|
});
|
||||||
@@ -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.
|
||||||
|
"""
|
||||||
|
|||||||
702
packages/sdk-python/agentlens/integrations/anthropic.py
Normal file
702
packages/sdk-python/agentlens/integrations/anthropic.py
Normal 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
|
||||||
@@ -26,16 +26,34 @@ logger = logging.getLogger("agentlens")
|
|||||||
|
|
||||||
# Cost per 1K tokens (input/output) for common models
|
# Cost per 1K tokens (input/output) for common models
|
||||||
_MODEL_COSTS: Dict[str, tuple] = {
|
_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": (0.03, 0.06),
|
||||||
"gpt-4-32k": (0.06, 0.12),
|
"gpt-4-32k": (0.06, 0.12),
|
||||||
"gpt-4-turbo": (0.01, 0.03),
|
"gpt-4-turbo": (0.01, 0.03),
|
||||||
"gpt-4-turbo-2024-04-09": (0.01, 0.03),
|
"gpt-4-turbo-2024-04-09": (0.01, 0.03),
|
||||||
"gpt-4-turbo-preview": (0.01, 0.03),
|
"gpt-4-turbo-preview": (0.01, 0.03),
|
||||||
"gpt-4o": (0.005, 0.015),
|
# GPT-3.5 family
|
||||||
"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-turbo": (0.0005, 0.0015),
|
"gpt-3.5-turbo": (0.0005, 0.0015),
|
||||||
"gpt-3.5-turbo-0125": (0.0005, 0.0015),
|
"gpt-3.5-turbo-0125": (0.0005, 0.0015),
|
||||||
"gpt-3.5-turbo-1106": (0.001, 0.002),
|
"gpt-3.5-turbo-1106": (0.001, 0.002),
|
||||||
|
|||||||
124
packages/sdk-ts/README.md
Normal file
124
packages/sdk-ts/README.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# agentlens-sdk
|
||||||
|
|
||||||
|
TypeScript SDK for AgentLens — Agent observability that traces decisions, not just API calls.
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/agentlens-sdk)
|
||||||
|
[](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
|
||||||
56
packages/sdk-ts/package.json
Normal file
56
packages/sdk-ts/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
packages/sdk-ts/src/_registry.ts
Normal file
11
packages/sdk-ts/src/_registry.ts
Normal 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;
|
||||||
|
}
|
||||||
29
packages/sdk-ts/src/decision.ts
Normal file
29
packages/sdk-ts/src/decision.ts
Normal 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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
76
packages/sdk-ts/src/index.ts
Normal file
76
packages/sdk-ts/src/index.ts
Normal 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";
|
||||||
136
packages/sdk-ts/src/models.ts
Normal file
136
packages/sdk-ts/src/models.ts
Normal 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();
|
||||||
|
}
|
||||||
185
packages/sdk-ts/src/trace.ts
Normal file
185
packages/sdk-ts/src/trace.ts
Normal 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
77
packages/sdk-ts/src/transport.ts
Normal file
77
packages/sdk-ts/src/transport.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
packages/sdk-ts/tsconfig.json
Normal file
21
packages/sdk-ts/tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
9
packages/sdk-ts/tsup.config.ts
Normal file
9
packages/sdk-ts/tsup.config.ts
Normal 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,
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user