From b064a3e4813f8dd4f559da030b4d84e3fcc4a82f Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Sat, 3 Jan 2026 17:05:45 +1000 Subject: [PATCH] Add agent logs and stats endpoints --- .../web/src/app/api/agents/[id]/logs/route.ts | 172 +++++++ .../src/app/api/agents/[id]/stats/route.ts | 269 +++++++++++ apps/web/src/lib/agents/build-tools.ts | 5 +- apps/web/src/test/agents.test.ts | 423 ++++++++++++++++++ 4 files changed, 868 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/app/api/agents/[id]/logs/route.ts create mode 100644 apps/web/src/app/api/agents/[id]/stats/route.ts create mode 100644 apps/web/src/test/agents.test.ts diff --git a/apps/web/src/app/api/agents/[id]/logs/route.ts b/apps/web/src/app/api/agents/[id]/logs/route.ts new file mode 100644 index 0000000..c274479 --- /dev/null +++ b/apps/web/src/app/api/agents/[id]/logs/route.ts @@ -0,0 +1,172 @@ +/** + * Agent Conversation Logs Endpoint + * + * GET: Retrieve recent conversation activity logs for an agent + */ + +import { prisma } from '@tpmjs/db'; +import { type NextRequest, NextResponse } from 'next/server'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; +export const maxDuration = 30; + +type RouteContext = { + params: Promise<{ id: string }>; +}; + +interface LogEntry { + id: string; + timestamp: string; + type: 'message' | 'tool_call' | 'tool_result' | 'conversation_start'; + conversationId: string; + conversationSlug: string; + role?: string; + content?: string; + toolName?: string; + toolCallId?: string; + inputTokens?: number; + outputTokens?: number; +} + +/** + * GET /api/agents/[id]/logs + * Retrieve recent conversation activity logs (accepts id or uid) + */ +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complex query logic needed for flexible log filtering +export async function GET(request: NextRequest, context: RouteContext): Promise { + const { id: idOrUid } = await context.params; + const { searchParams } = new URL(request.url); + + const limit = Math.min(Number.parseInt(searchParams.get('limit') || '100', 10), 500); + const offset = Number.parseInt(searchParams.get('offset') || '0', 10); + const conversationSlug = searchParams.get('conversation') || undefined; + const type = searchParams.get('type') || undefined; // 'message' | 'tool_call' | 'all' + + try { + // Fetch agent by id or uid + const agent = await prisma.agent.findFirst({ + where: { + OR: [{ id: idOrUid }, { uid: idOrUid }], + }, + select: { id: true, name: true, uid: true }, + }); + + if (!agent) { + return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 }); + } + + // Build role filter based on type + const roleFilter = + type === 'tool_call' + ? { in: ['TOOL' as const] } + : type === 'message' + ? { in: ['USER' as const, 'ASSISTANT' as const] } + : undefined; + + // Fetch messages with conversation info + const messages = await prisma.message.findMany({ + where: { + conversation: { + agentId: agent.id, + ...(conversationSlug && { slug: conversationSlug }), + }, + ...(roleFilter && { role: roleFilter }), + }, + orderBy: { createdAt: 'desc' }, + take: limit + 1, + skip: offset, + include: { + conversation: { + select: { + id: true, + slug: true, + title: true, + }, + }, + }, + }); + + const hasMore = messages.length > limit; + const data = hasMore ? messages.slice(0, limit) : messages; + + // Transform to log entries + const logs: LogEntry[] = data.map((msg) => { + const base = { + id: msg.id, + timestamp: msg.createdAt.toISOString(), + conversationId: msg.conversation.id, + conversationSlug: msg.conversation.slug, + }; + + if (msg.role === 'TOOL') { + return { + ...base, + type: 'tool_result' as const, + toolName: msg.toolName || undefined, + toolCallId: msg.toolCallId || undefined, + content: msg.content.slice(0, 500), // Truncate for logs view + }; + } + + return { + ...base, + type: 'message' as const, + role: msg.role, + content: msg.content.slice(0, 500), // Truncate for logs view + inputTokens: msg.inputTokens || undefined, + outputTokens: msg.outputTokens || undefined, + }; + }); + + // Get unique tool calls from assistant messages + const assistantMessages = data.filter((m) => m.role === 'ASSISTANT' && m.toolCalls); + const toolCallLogs: LogEntry[] = []; + + for (const msg of assistantMessages) { + if (msg.toolCalls && Array.isArray(msg.toolCalls)) { + for (const tc of msg.toolCalls as Array<{ + id?: string; + toolCallId?: string; + name?: string; + toolName?: string; + }>) { + toolCallLogs.push({ + id: `${msg.id}-${tc.id || tc.toolCallId}`, + timestamp: msg.createdAt.toISOString(), + type: 'tool_call', + conversationId: msg.conversation.id, + conversationSlug: msg.conversation.slug, + toolName: tc.name || tc.toolName, + toolCallId: tc.id || tc.toolCallId, + }); + } + } + } + + // Merge and sort all logs + const allLogs = [...logs, ...toolCallLogs].sort( + (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() + ); + + return NextResponse.json({ + success: true, + data: { + agent: { + id: agent.id, + uid: agent.uid, + name: agent.name, + }, + logs: allLogs.slice(0, limit), + pagination: { + limit, + offset, + hasMore, + }, + }, + }); + } catch (error) { + console.error('Failed to fetch agent logs:', error); + return NextResponse.json({ success: false, error: 'Failed to fetch logs' }, { status: 500 }); + } +} diff --git a/apps/web/src/app/api/agents/[id]/stats/route.ts b/apps/web/src/app/api/agents/[id]/stats/route.ts new file mode 100644 index 0000000..cad3ce5 --- /dev/null +++ b/apps/web/src/app/api/agents/[id]/stats/route.ts @@ -0,0 +1,269 @@ +/** + * Agent Statistics Endpoint + * + * GET: Retrieve aggregated statistics for an agent's conversations + */ + +import { prisma } from '@tpmjs/db'; +import { type NextRequest, NextResponse } from 'next/server'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; +export const maxDuration = 30; + +type RouteContext = { + params: Promise<{ id: string }>; +}; + +interface AgentStats { + agent: { + id: string; + uid: string; + name: string; + provider: string; + modelId: string; + createdAt: string; + }; + conversations: { + total: number; + last24h: number; + last7d: number; + last30d: number; + }; + messages: { + total: number; + byRole: { + user: number; + assistant: number; + tool: number; + system: number; + }; + last24h: number; + last7d: number; + }; + tokens: { + totalInput: number; + totalOutput: number; + total: number; + averagePerConversation: number; + last24h: { + input: number; + output: number; + }; + last7d: { + input: number; + output: number; + }; + }; + tools: { + totalCalls: number; + uniqueToolsUsed: number; + topTools: Array<{ + name: string; + count: number; + }>; + last24h: number; + }; + activity: { + firstConversation: string | null; + lastConversation: string | null; + averageMessagesPerConversation: number; + longestConversation: { + id: string; + slug: string; + messageCount: number; + } | null; + }; +} + +/** + * GET /api/agents/[id]/stats + * Retrieve aggregated statistics for an agent (accepts id or uid) + */ +export async function GET(_request: NextRequest, context: RouteContext): Promise { + const { id: idOrUid } = await context.params; + + try { + // Fetch agent by id or uid + const agent = await prisma.agent.findFirst({ + where: { + OR: [{ id: idOrUid }, { uid: idOrUid }], + }, + select: { + id: true, + uid: true, + name: true, + provider: true, + modelId: true, + createdAt: true, + }, + }); + + if (!agent) { + return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 }); + } + + const now = new Date(); + const last24h = new Date(now.getTime() - 24 * 60 * 60 * 1000); + const last7d = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + const last30d = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + + // Conversation stats + const [totalConversations, convLast24h, convLast7d, convLast30d] = await Promise.all([ + prisma.conversation.count({ where: { agentId: agent.id } }), + prisma.conversation.count({ where: { agentId: agent.id, createdAt: { gte: last24h } } }), + prisma.conversation.count({ where: { agentId: agent.id, createdAt: { gte: last7d } } }), + prisma.conversation.count({ where: { agentId: agent.id, createdAt: { gte: last30d } } }), + ]); + + // Message stats + const [totalMessages, userMessages, assistantMessages, toolMessages, msgLast24h, msgLast7d] = + await Promise.all([ + prisma.message.count({ where: { conversation: { agentId: agent.id } } }), + prisma.message.count({ where: { conversation: { agentId: agent.id }, role: 'USER' } }), + prisma.message.count({ where: { conversation: { agentId: agent.id }, role: 'ASSISTANT' } }), + prisma.message.count({ where: { conversation: { agentId: agent.id }, role: 'TOOL' } }), + prisma.message.count({ + where: { conversation: { agentId: agent.id }, createdAt: { gte: last24h } }, + }), + prisma.message.count({ + where: { conversation: { agentId: agent.id }, createdAt: { gte: last7d } }, + }), + ]); + + // Token stats using aggregate + const [tokenTotals, tokensLast24h, tokensLast7d] = await Promise.all([ + prisma.message.aggregate({ + where: { conversation: { agentId: agent.id } }, + _sum: { inputTokens: true, outputTokens: true }, + }), + prisma.message.aggregate({ + where: { conversation: { agentId: agent.id }, createdAt: { gte: last24h } }, + _sum: { inputTokens: true, outputTokens: true }, + }), + prisma.message.aggregate({ + where: { conversation: { agentId: agent.id }, createdAt: { gte: last7d } }, + _sum: { inputTokens: true, outputTokens: true }, + }), + ]); + + const totalInput = tokenTotals._sum.inputTokens || 0; + const totalOutput = tokenTotals._sum.outputTokens || 0; + + // Tool usage stats + const toolMessages2 = await prisma.message.findMany({ + where: { conversation: { agentId: agent.id }, role: 'TOOL' }, + select: { toolName: true, createdAt: true }, + }); + + const toolCounts: Record = {}; + let toolLast24h = 0; + + for (const tm of toolMessages2) { + if (tm.toolName) { + toolCounts[tm.toolName] = (toolCounts[tm.toolName] || 0) + 1; + } + if (tm.createdAt >= last24h) { + toolLast24h++; + } + } + + const topTools = Object.entries(toolCounts) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); + + // Activity stats + const [firstConv, lastConv, longestConv] = await Promise.all([ + prisma.conversation.findFirst({ + where: { agentId: agent.id }, + orderBy: { createdAt: 'asc' }, + select: { createdAt: true }, + }), + prisma.conversation.findFirst({ + where: { agentId: agent.id }, + orderBy: { createdAt: 'desc' }, + select: { createdAt: true }, + }), + prisma.conversation.findFirst({ + where: { agentId: agent.id }, + orderBy: { messages: { _count: 'desc' } }, + select: { + id: true, + slug: true, + _count: { select: { messages: true } }, + }, + }), + ]); + + const stats: AgentStats = { + agent: { + id: agent.id, + uid: agent.uid, + name: agent.name, + provider: agent.provider, + modelId: agent.modelId, + createdAt: agent.createdAt.toISOString(), + }, + conversations: { + total: totalConversations, + last24h: convLast24h, + last7d: convLast7d, + last30d: convLast30d, + }, + messages: { + total: totalMessages, + byRole: { + user: userMessages, + assistant: assistantMessages, + tool: toolMessages, + system: 0, // System messages are inline, not stored separately + }, + last24h: msgLast24h, + last7d: msgLast7d, + }, + tokens: { + totalInput, + totalOutput, + total: totalInput + totalOutput, + averagePerConversation: + totalConversations > 0 ? Math.round((totalInput + totalOutput) / totalConversations) : 0, + last24h: { + input: tokensLast24h._sum.inputTokens || 0, + output: tokensLast24h._sum.outputTokens || 0, + }, + last7d: { + input: tokensLast7d._sum.inputTokens || 0, + output: tokensLast7d._sum.outputTokens || 0, + }, + }, + tools: { + totalCalls: toolMessages, + uniqueToolsUsed: Object.keys(toolCounts).length, + topTools, + last24h: toolLast24h, + }, + activity: { + firstConversation: firstConv?.createdAt.toISOString() || null, + lastConversation: lastConv?.createdAt.toISOString() || null, + averageMessagesPerConversation: + totalConversations > 0 ? Math.round(totalMessages / totalConversations) : 0, + longestConversation: longestConv + ? { + id: longestConv.id, + slug: longestConv.slug, + messageCount: longestConv._count.messages, + } + : null, + }, + }; + + return NextResponse.json({ + success: true, + data: stats, + }); + } catch (error) { + console.error('Failed to fetch agent stats:', error); + return NextResponse.json({ success: false, error: 'Failed to fetch stats' }, { status: 500 }); + } +} diff --git a/apps/web/src/lib/agents/build-tools.ts b/apps/web/src/lib/agents/build-tools.ts index ec34c33..48841e3 100644 --- a/apps/web/src/lib/agents/build-tools.ts +++ b/apps/web/src/lib/agents/build-tools.ts @@ -135,9 +135,12 @@ export async function fetchAgentByIdOrUidWithTools( /** * Sanitize npm package name to valid tool name + * OpenAI limits tool names to 64 characters */ function sanitizeToolName(name: string): string { - return name.replace(/[@/]/g, '-').replace(/^-+/, ''); + const sanitized = name.replace(/[@/]/g, '-').replace(/^-+/, ''); + // Truncate to 64 chars (OpenAI limit) + return sanitized.slice(0, 64); } /** diff --git a/apps/web/src/test/agents.test.ts b/apps/web/src/test/agents.test.ts new file mode 100644 index 0000000..c8b2194 --- /dev/null +++ b/apps/web/src/test/agents.test.ts @@ -0,0 +1,423 @@ +/** + * Agent Endpoints Integration Tests + * + * Tests the agent conversation API endpoints including: + * - Sending messages and receiving streaming responses + * - Tool calls and execution + * - Conversation history persistence + * - Database state verification + */ + +import { prisma } from '@tpmjs/db'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const BASE_URL = process.env.TEST_BASE_URL || 'http://localhost:3000'; +const TEST_AGENT_UID = 'omega'; // The test agent UID +const TEST_CONVERSATION_SLUG = `test-conv-${Date.now()}`; + +interface SSEEvent { + event: string; + data: unknown; +} + +/** + * Parse SSE response into array of events + */ +function parseSSEResponse(text: string): SSEEvent[] { + const events: SSEEvent[] = []; + const lines = text.split('\n'); + + let currentEvent = ''; + let currentData = ''; + + for (const line of lines) { + if (line.startsWith('event: ')) { + currentEvent = line.slice(7); + } else if (line.startsWith('data: ')) { + currentData = line.slice(6); + if (currentEvent && currentData) { + try { + events.push({ + event: currentEvent, + data: JSON.parse(currentData), + }); + } catch { + events.push({ + event: currentEvent, + data: currentData, + }); + } + currentEvent = ''; + currentData = ''; + } + } + } + + return events; +} + +describe('Agent Endpoints', () => { + let testAgent: { + id: string; + uid: string; + name: string; + userId: string; + provider: string; + } | null = null; + + let testConversationId: string | null = null; + + beforeAll(async () => { + // Find the test agent in the database + testAgent = await prisma.agent.findUnique({ + where: { uid: TEST_AGENT_UID }, + select: { + id: true, + uid: true, + name: true, + userId: true, + provider: true, + }, + }); + + if (!testAgent) { + console.warn(`⚠️ Test agent "${TEST_AGENT_UID}" not found. Skipping agent tests.`); + } + }); + + afterAll(async () => { + // Clean up test conversation + if (testConversationId) { + await prisma.conversation.deleteMany({ + where: { id: testConversationId }, + }); + } + await prisma.$disconnect(); + }); + + describe('POST /api/agents/[id]/conversation/[conversationId]', () => { + it('should create a new conversation and receive a response', async () => { + if (!testAgent) { + console.log('Skipping: No test agent available'); + return; + } + + const response = await fetch( + `${BASE_URL}/api/agents/${testAgent.uid}/conversation/${TEST_CONVERSATION_SLUG}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: 'Hello, what is 2 + 2?' }), + } + ); + + expect(response.ok).toBe(true); + expect(response.headers.get('content-type')).toContain('text/event-stream'); + + const text = await response.text(); + const events = parseSSEResponse(text); + + // Should have at least chunk events and a complete event + expect(events.length).toBeGreaterThan(0); + + // Find the complete event + const completeEvent = events.find((e) => e.event === 'complete'); + expect(completeEvent).toBeDefined(); + + if (completeEvent?.data && typeof completeEvent.data === 'object') { + const data = completeEvent.data as { conversationId?: string }; + testConversationId = data.conversationId || null; + } + + // Find text chunks + const textChunks = events.filter((e) => e.event === 'chunk'); + expect(textChunks.length).toBeGreaterThan(0); + + // Verify token usage was reported + const tokenEvent = events.find((e) => e.event === 'tokens'); + expect(tokenEvent).toBeDefined(); + if (tokenEvent?.data && typeof tokenEvent.data === 'object') { + const tokens = tokenEvent.data as { inputTokens?: number; outputTokens?: number }; + expect(tokens.inputTokens).toBeGreaterThan(0); + expect(tokens.outputTokens).toBeGreaterThan(0); + } + }); + + it('should persist conversation in database', async () => { + if (!testAgent || !testConversationId) { + console.log('Skipping: No test conversation available'); + return; + } + + const conversation = await prisma.conversation.findUnique({ + where: { id: testConversationId }, + include: { + messages: { + orderBy: { createdAt: 'asc' }, + }, + }, + }); + + expect(conversation).toBeDefined(); + expect(conversation?.agentId).toBe(testAgent.id); + expect(conversation?.slug).toBe(TEST_CONVERSATION_SLUG); + + // Should have at least a user message and an assistant message + expect(conversation?.messages.length).toBeGreaterThanOrEqual(2); + + const userMessage = conversation?.messages.find((m) => m.role === 'USER'); + expect(userMessage?.content).toContain('2 + 2'); + + const assistantMessage = conversation?.messages.find((m) => m.role === 'ASSISTANT'); + expect(assistantMessage).toBeDefined(); + expect(assistantMessage?.content.length).toBeGreaterThan(0); + }); + + it('should maintain conversation history on follow-up messages', async () => { + if (!testAgent) { + console.log('Skipping: No test agent available'); + return; + } + + const response = await fetch( + `${BASE_URL}/api/agents/${testAgent.uid}/conversation/${TEST_CONVERSATION_SLUG}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: 'What was my previous question?' }), + } + ); + + expect(response.ok).toBe(true); + + const text = await response.text(); + const events = parseSSEResponse(text); + + // Collect all text from chunks + const textContent = events + .filter((e) => e.event === 'chunk') + .map((e) => { + const data = e.data as { text?: string }; + return data.text || ''; + }) + .join(''); + + // The response should reference the previous question about 2+2 + expect(textContent.toLowerCase()).toMatch(/2.*\+.*2|previous|earlier|asked/); + }); + + it('should execute tool calls when requested', async () => { + if (!testAgent) { + console.log('Skipping: No test agent available'); + return; + } + + // Use a unique conversation for tool test + const toolConvSlug = `tool-test-${Date.now()}`; + + const response = await fetch( + `${BASE_URL}/api/agents/${testAgent.uid}/conversation/${toolConvSlug}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + message: 'Please execute this Python code: print("Hello from test")', + }), + } + ); + + expect(response.ok).toBe(true); + + const text = await response.text(); + const events = parseSSEResponse(text); + + // Should have tool call events if the agent has code execution tools + const toolCallEvents = events.filter((e) => e.event === 'tool_call'); + const toolResultEvents = events.filter((e) => e.event === 'tool_result'); + + if (toolCallEvents.length > 0 && toolCallEvents[0]) { + console.log(`✓ Tool called: ${JSON.stringify(toolCallEvents[0].data)}`); + // Tool results may come in same stream or be processed async + // The important thing is the tool was called + console.log(` Tool results received: ${toolResultEvents.length}`); + + // Verify tool call was saved to database + const conversation = await prisma.conversation.findFirst({ + where: { + agentId: testAgent.id, + slug: toolConvSlug, + }, + include: { + messages: true, + }, + }); + + const toolMessage = conversation?.messages.find((m) => m.role === 'TOOL'); + if (toolMessage) { + expect(toolMessage.toolName).toBeDefined(); + expect(toolMessage.toolCallId).toBeDefined(); + } + + // Clean up + await prisma.conversation.deleteMany({ + where: { agentId: testAgent.id, slug: toolConvSlug }, + }); + } else { + console.log('ℹ️ No tool calls made (agent may not have tool access)'); + } + }); + }); + + describe('GET /api/agents/[id]/conversation/[conversationId]', () => { + it('should retrieve conversation history', async () => { + if (!testAgent || !testConversationId) { + console.log('Skipping: No test conversation available'); + return; + } + + const response = await fetch( + `${BASE_URL}/api/agents/${testAgent.uid}/conversation/${TEST_CONVERSATION_SLUG}`, + { method: 'GET' } + ); + + expect(response.ok).toBe(true); + + const result = await response.json(); + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + expect(result.data.slug).toBe(TEST_CONVERSATION_SLUG); + expect(result.data.messages).toBeInstanceOf(Array); + expect(result.data.messages.length).toBeGreaterThanOrEqual(2); + }); + + it('should return 404 for non-existent conversation', async () => { + if (!testAgent) { + console.log('Skipping: No test agent available'); + return; + } + + const response = await fetch( + `${BASE_URL}/api/agents/${testAgent.uid}/conversation/non-existent-conv-12345`, + { method: 'GET' } + ); + + expect(response.status).toBe(404); + }); + }); + + describe('GET /api/agents/[id]/conversations', () => { + it('should list all conversations for an agent', async () => { + if (!testAgent) { + console.log('Skipping: No test agent available'); + return; + } + + const response = await fetch(`${BASE_URL}/api/agents/${testAgent.id}/conversations`, { + method: 'GET', + }); + + expect(response.ok).toBe(true); + + const result = await response.json(); + expect(result.success).toBe(true); + expect(result.data).toBeInstanceOf(Array); + expect(result.pagination).toBeDefined(); + + // Our test conversation should be in the list + const testConv = result.data.find((c: { slug: string }) => c.slug === TEST_CONVERSATION_SLUG); + expect(testConv).toBeDefined(); + expect(testConv.messageCount).toBeGreaterThanOrEqual(2); + }); + + it('should support pagination', async () => { + if (!testAgent) { + console.log('Skipping: No test agent available'); + return; + } + + const response = await fetch( + `${BASE_URL}/api/agents/${testAgent.id}/conversations?limit=1&offset=0`, + { method: 'GET' } + ); + + expect(response.ok).toBe(true); + + const result = await response.json(); + expect(result.pagination.limit).toBe(1); + expect(result.data.length).toBeLessThanOrEqual(1); + }); + }); + + describe('Database State Verification', () => { + it('should have correct message token counts', async () => { + if (!testConversationId) { + console.log('Skipping: No test conversation available'); + return; + } + + const messages = await prisma.message.findMany({ + where: { conversationId: testConversationId }, + orderBy: { createdAt: 'asc' }, + }); + + // Assistant messages should have token counts + const assistantMessages = messages.filter((m) => m.role === 'ASSISTANT'); + for (const msg of assistantMessages) { + expect(msg.inputTokens).toBeGreaterThan(0); + expect(msg.outputTokens).toBeGreaterThan(0); + } + }); + + it('should correctly track tool calls in messages', async () => { + if (!testConversationId) { + console.log('Skipping: No test conversation available'); + return; + } + + const toolMessages = await prisma.message.findMany({ + where: { + conversationId: testConversationId, + role: 'TOOL', + }, + }); + + for (const msg of toolMessages) { + expect(msg.toolCallId).toBeDefined(); + expect(msg.toolName).toBeDefined(); + expect(msg.toolResult).toBeDefined(); + } + }); + }); + + describe('Agent with lookup by ID or UID', () => { + it('should accept agent ID in URL', async () => { + if (!testAgent) { + console.log('Skipping: No test agent available'); + return; + } + + const response = await fetch(`${BASE_URL}/api/agents/${testAgent.id}/conversations`, { + method: 'GET', + }); + + expect(response.ok).toBe(true); + const result = await response.json(); + expect(result.success).toBe(true); + }); + + it('should accept agent UID in URL', async () => { + if (!testAgent) { + console.log('Skipping: No test agent available'); + return; + } + + const response = await fetch(`${BASE_URL}/api/agents/${testAgent.uid}/conversations`, { + method: 'GET', + }); + + expect(response.ok).toBe(true); + const result = await response.json(); + expect(result.success).toBe(true); + }); + }); +});