import { TraceBuilder, SpanType, SpanStatus, DecisionType, nowISO, getClient, } 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(); private toolCalls = new Map(); private rootSpans = new Map(); startSession( sessionId: string, metadata?: Record, ): 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 || call.tool || "unknown-tool", 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 }), }); const reasoningText = title !== call.tool && title ? `Selected ${call.tool}: ${title}` : `Selected tool: ${call.tool}`; trace.addDecision({ type: DecisionType.TOOL_SELECTION, chosen: call.tool as JsonValue, alternatives: [], reasoning: reasoningText, 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 | 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); } getActiveSessionIds(): string[] { return Array.from(this.traces.keys()); } flushSession(sessionId: string): 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: SpanStatus.COMPLETED, endedAt: nowISO(), }); } trace.end({ status: "COMPLETED" }); const transport = getClient(); if (transport) { transport.add(trace.toPayload()); } this.traces.delete(sessionId); this.rootSpans.delete(sessionId); } }