diff --git a/packages/client/src/components/agent-action-viewer.tsx b/packages/client/src/components/agent-action-viewer.tsx index 8df5ba29d29e..e12642e4c750 100644 --- a/packages/client/src/components/agent-action-viewer.tsx +++ b/packages/client/src/components/agent-action-viewer.tsx @@ -40,6 +40,7 @@ const ITEMS_PER_PAGE = 15; enum ActionType { all = 'all', llm = 'llm', + embedding = 'embedding', transcription = 'transcription', image = 'image', other = 'other', @@ -59,7 +60,9 @@ type AgentActionViewerProps = { // Helper functions function getModelUsageType(modelType: string): string { if ( - (modelType.includes('TEXT') || modelType.includes('OBJECT')) && + (modelType.includes('TEXT') || + modelType.includes('OBJECT') || + modelType.includes('REASONING')) && !modelType.includes('EMBEDDING') && !modelType.includes('TRANSCRIPTION') ) { @@ -74,15 +77,7 @@ function getModelUsageType(modelType: string): string { if (modelType.includes('IMAGE')) { return 'Image'; } - if ( - !modelType.includes('TEXT') && - !modelType.includes('IMAGE') && - !modelType.includes('EMBEDDING') && - !modelType.includes('TRANSCRIPTION') - ) { - return 'Other'; - } - return 'Unknown'; + return 'Other'; } function formatDate(timestamp: number | undefined) { @@ -164,9 +159,10 @@ function ActionCard({ action, onDelete }: ActionCardProps) { const modelType = action.body?.modelType || ''; const modelKey = action.body?.modelKey || ''; - const isActionLog = action.type === 'action'; + const logType = action.type || ''; + const isActionLog = logType === 'action'; const actionName = action.body?.action || ''; - const IconComponent = getModelIcon(isActionLog ? 'ACTION' : modelType); + const IconComponent = getModelIcon(isActionLog ? 'ACTION' : modelType || logType); const usageType = isActionLog ? 'Action' : getModelUsageType(modelType); const responseObj = typeof action.body?.response === 'object' ? action.body.response : undefined; const tokenUsage = formatTokenUsage(responseObj?.usage || action.body?.usage); @@ -361,7 +357,7 @@ function ActionCard({ action, onDelete }: ActionCardProps) {

{isActionLog ? actionName : usageType}

- {isActionLog ? 'Action' : modelType} + {isActionLog ? 'Action' : modelType || logType} {action.body?.promptCount && action.body.promptCount > 1 && ( @@ -601,8 +597,11 @@ export function AgentActionViewer({ agentId, roomId }: AgentActionViewerProps) { switch (selectedType) { case ActionType.llm: - // Include both LLM calls and actions (which often contain LLM prompts) - if (usageType !== 'LLM' && !isActionLog) return false; + // Only show LLM model calls (not actions) + if (usageType !== 'LLM') return false; + break; + case ActionType.embedding: + if (usageType !== 'Embedding') return false; break; case ActionType.transcription: if (usageType !== 'Transcription') return false; @@ -611,6 +610,7 @@ export function AgentActionViewer({ agentId, roomId }: AgentActionViewerProps) { if (usageType !== 'Image') return false; break; case ActionType.other: + // "Other" includes actions and unknown model types if (usageType !== 'Other' && usageType !== 'Unknown' && !isActionLog) return false; break; } @@ -761,6 +761,7 @@ export function AgentActionViewer({ agentId, roomId }: AgentActionViewerProps) { All Actions LLM Calls + Embeddings Transcriptions Image Operations Other diff --git a/packages/client/src/hooks/use-query-hooks.ts b/packages/client/src/hooks/use-query-hooks.ts index b9e6d75375d7..d0c5058cc202 100644 --- a/packages/client/src/hooks/use-query-hooks.ts +++ b/packages/client/src/hooks/use-query-hooks.ts @@ -614,8 +614,15 @@ export function useAgentActions(agentId: UUID, roomId?: UUID, excludeTypes?: str const response = await getClient().agents.getAgentLogs(agentId, { limit: 50, }); - // Map the API logs to client format - return response ? response.map(mapApiLogToClient) : []; + if (!response) return []; + + // Filter to only include model calls (useModel:*) and actions + const relevantLogs = response.filter((log) => { + const logType = log.type || ''; + return logType.startsWith('useModel:') || logType === 'action'; + }); + + return relevantLogs.map(mapApiLogToClient); }, refetchInterval: 1000, staleTime: 1000, diff --git a/packages/core/src/__tests__/runtime-streaming.test.ts b/packages/core/src/__tests__/runtime-streaming.test.ts index ff2871a83aa3..43a7979a54a7 100644 --- a/packages/core/src/__tests__/runtime-streaming.test.ts +++ b/packages/core/src/__tests__/runtime-streaming.test.ts @@ -403,4 +403,73 @@ describe('useModel Streaming', () => { expect(result).toBe('XYZ'); }); }); + + describe('database logging', () => { + it('should log streaming model calls to database', async () => { + const mockChunks = ['Hello', ' ', 'World']; + const mockAdapter = createMockAdapter(); + + const streamingRuntime = new AgentRuntime({ + agentId: stringToUuid('test-logging-agent'), + character: mockCharacter, + adapter: mockAdapter, + }); + + streamingRuntime.registerModel( + ModelType.TEXT_LARGE, + async (_rt, params) => { + if ((params as any).stream) { + return createMockTextStreamResult(mockChunks); + } + return mockChunks.join(''); + }, + 'test-provider' + ); + + await streamingRuntime.useModel(ModelType.TEXT_LARGE, { + prompt: 'Test prompt', + onStreamChunk: () => {}, + }); + + // Verify adapter.log was called + const logCalls = (mockAdapter.log as any).mock.calls; + expect(logCalls.length).toBeGreaterThan(0); + + // Verify the log contains correct model info + const logCall = logCalls[0][0]; + expect(logCall.type).toBe('useModel:TEXT_LARGE'); + expect(logCall.body.modelKey).toBe('TEXT_LARGE'); + expect(logCall.body.response).toBe('Hello World'); + }); + + it('should log non-streaming model calls to database', async () => { + const mockAdapter = createMockAdapter(); + + const nonStreamingRuntime = new AgentRuntime({ + agentId: stringToUuid('test-logging-agent-2'), + character: mockCharacter, + adapter: mockAdapter, + }); + + nonStreamingRuntime.registerModel( + ModelType.TEXT_LARGE, + async () => 'Non-streamed response', + 'test-provider' + ); + + await nonStreamingRuntime.useModel(ModelType.TEXT_LARGE, { + prompt: 'Test prompt', + }); + + // Verify adapter.log was called + const logCalls = (mockAdapter.log as any).mock.calls; + expect(logCalls.length).toBeGreaterThan(0); + + // Verify the log contains correct model info + const logCall = logCalls[0][0]; + expect(logCall.type).toBe('useModel:TEXT_LARGE'); + expect(logCall.body.modelKey).toBe('TEXT_LARGE'); + expect(logCall.body.response).toBe('Non-streamed response'); + }); + }); }); diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index 96d9bab869d9..0f09337c795c 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -2166,6 +2166,61 @@ export class AgentRuntime implements IAgentRuntime { return Object.keys(modelSettings).length > 0 ? modelSettings : null; } + /** + * Helper to log model calls to the database (used by both streaming and non-streaming paths) + */ + private logModelCall( + modelType: string, + modelKey: string, + params: unknown, + promptContent: string | null, + elapsedTime: number, + provider: string | undefined, + response: unknown + ): void { + // Log prompts to action context (except embeddings) + if (modelKey !== ModelType.TEXT_EMBEDDING && promptContent) { + if (this.currentActionContext) { + this.currentActionContext.prompts.push({ + modelType: modelKey, + prompt: promptContent, + timestamp: Date.now(), + }); + } + } + + // Log to database + this.adapter.log({ + entityId: this.agentId, + roomId: this.currentRoomId ?? this.agentId, + body: { + modelType, + modelKey, + params: { + ...(typeof params === 'object' && !Array.isArray(params) && params ? params : {}), + prompt: promptContent, + }, + prompt: promptContent, + systemPrompt: this.character?.system || null, + runId: this.getCurrentRunId(), + timestamp: Date.now(), + executionTime: elapsedTime, + provider: provider || this.models.get(modelKey)?.[0]?.provider || 'unknown', + actionContext: this.currentActionContext + ? { + actionName: this.currentActionContext.actionName, + actionId: this.currentActionContext.actionId, + } + : undefined, + response: + Array.isArray(response) && response.every((x) => typeof x === 'number') + ? '[array]' + : response, + }, + type: `useModel:${modelKey}`, + }); + } + async useModel( modelType: T, params: ModelParamsMap[T], @@ -2332,6 +2387,15 @@ export class AgentRuntime implements IAgentRuntime { 'Model output (stream with callback complete)' ); + this.logModelCall( + modelType, + modelKey, + params, + promptContent, + elapsedTime, + provider, + fullText + ); return fullText as R; } @@ -2351,49 +2415,7 @@ export class AgentRuntime implements IAgentRuntime { 'Model output' ); - // Log all prompts except TEXT_EMBEDDING to track agent behavior - if (modelKey !== ModelType.TEXT_EMBEDDING && promptContent) { - // If we're in an action context, collect the prompt - if (this.currentActionContext) { - this.currentActionContext.prompts.push({ - modelType: modelKey, - prompt: promptContent, - timestamp: Date.now(), - }); - } - } - - // Keep the existing model logging for backward compatibility - this.adapter.log({ - entityId: this.agentId, - roomId: this.currentRoomId ?? this.agentId, - body: { - modelType, - modelKey, - params: { - ...(typeof params === 'object' && !Array.isArray(params) && params ? params : {}), - prompt: promptContent, - }, - prompt: promptContent, - systemPrompt: this.character?.system || null, - runId: this.getCurrentRunId(), - timestamp: Date.now(), - executionTime: elapsedTime, - provider: provider || this.models.get(modelKey)?.[0]?.provider || 'unknown', - actionContext: this.currentActionContext - ? { - actionName: this.currentActionContext.actionName, - actionId: this.currentActionContext.actionId, - } - : undefined, - response: - Array.isArray(response) && response.every((x) => typeof x === 'number') - ? '[array]' - : response, - }, - type: `useModel:${modelKey}`, - }); - + this.logModelCall(modelType, modelKey, params, promptContent, elapsedTime, provider, response); return response as R; }