@robota-sdk/agent-plugin 3.0.0-beta.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/dist/node/index.cjs +1 -0
  3. package/dist/node/index.d.ts +1724 -0
  4. package/dist/node/index.d.ts.map +1 -0
  5. package/dist/node/index.js +2 -0
  6. package/dist/node/index.js.map +1 -0
  7. package/package.json +48 -0
  8. package/src/conversation-history/__tests__/conversation-history-plugin.test.ts +221 -0
  9. package/src/conversation-history/__tests__/history-storages.test.ts +115 -0
  10. package/src/conversation-history/conversation-history-helpers.ts +120 -0
  11. package/src/conversation-history/conversation-history-plugin.ts +294 -0
  12. package/src/conversation-history/index.ts +11 -0
  13. package/src/conversation-history/storages/database-storage.ts +96 -0
  14. package/src/conversation-history/storages/file-storage.ts +95 -0
  15. package/src/conversation-history/storages/index.ts +3 -0
  16. package/src/conversation-history/storages/memory-storage.ts +44 -0
  17. package/src/conversation-history/types.ts +64 -0
  18. package/src/error-handling/__tests__/error-handling-plugin.test.ts +201 -0
  19. package/src/error-handling/context-adapter.ts +48 -0
  20. package/src/error-handling/error-handling-helpers.ts +53 -0
  21. package/src/error-handling/error-handling-plugin.ts +293 -0
  22. package/src/error-handling/index.ts +9 -0
  23. package/src/error-handling/types.ts +82 -0
  24. package/src/execution-analytics/__tests__/execution-analytics-plugin.test.ts +224 -0
  25. package/src/execution-analytics/analytics-aggregation.ts +88 -0
  26. package/src/execution-analytics/execution-analytics-helpers.ts +83 -0
  27. package/src/execution-analytics/execution-analytics-plugin.ts +315 -0
  28. package/src/execution-analytics/index.ts +9 -0
  29. package/src/execution-analytics/types.ts +97 -0
  30. package/src/index.ts +8 -0
  31. package/src/limits/__tests__/limits-plugin.test.ts +712 -0
  32. package/src/limits/index.ts +9 -0
  33. package/src/limits/limits-helpers.ts +185 -0
  34. package/src/limits/limits-plugin.ts +196 -0
  35. package/src/limits/types.ts +73 -0
  36. package/src/limits/validation.ts +81 -0
  37. package/src/logging/__tests__/formatters.test.ts +48 -0
  38. package/src/logging/__tests__/logging-plugin.test.ts +464 -0
  39. package/src/logging/__tests__/logging-storages.test.ts +95 -0
  40. package/src/logging/formatters.ts +28 -0
  41. package/src/logging/index.ts +15 -0
  42. package/src/logging/logging-helpers.ts +223 -0
  43. package/src/logging/logging-plugin.ts +288 -0
  44. package/src/logging/storages/console-storage.ts +44 -0
  45. package/src/logging/storages/file-storage.ts +44 -0
  46. package/src/logging/storages/index.ts +4 -0
  47. package/src/logging/storages/remote-storage.ts +78 -0
  48. package/src/logging/storages/silent-storage.ts +18 -0
  49. package/src/logging/types.ts +106 -0
  50. package/src/performance/__tests__/memory-storage.test.ts +86 -0
  51. package/src/performance/__tests__/performance-plugin.test.ts +208 -0
  52. package/src/performance/__tests__/system-metrics-collector.test.ts +33 -0
  53. package/src/performance/collectors/system-metrics-collector.ts +69 -0
  54. package/src/performance/index.ts +12 -0
  55. package/src/performance/performance-helpers.ts +86 -0
  56. package/src/performance/performance-plugin.ts +274 -0
  57. package/src/performance/storages/index.ts +1 -0
  58. package/src/performance/storages/memory-storage.ts +88 -0
  59. package/src/performance/types.ts +160 -0
  60. package/src/usage/__tests__/aggregate-usage-stats.test.ts +136 -0
  61. package/src/usage/__tests__/memory-storage.test.ts +83 -0
  62. package/src/usage/__tests__/silent-storage.test.ts +44 -0
  63. package/src/usage/__tests__/usage-plugin-helpers.test.ts +155 -0
  64. package/src/usage/__tests__/usage-plugin.test.ts +358 -0
  65. package/src/usage/aggregate-usage-stats.ts +142 -0
  66. package/src/usage/index.ts +14 -0
  67. package/src/usage/storages/file-storage.ts +115 -0
  68. package/src/usage/storages/index.ts +4 -0
  69. package/src/usage/storages/memory-storage.ts +61 -0
  70. package/src/usage/storages/remote-storage.ts +143 -0
  71. package/src/usage/storages/silent-storage.ts +38 -0
  72. package/src/usage/types.ts +132 -0
  73. package/src/usage/usage-plugin-helpers.ts +116 -0
  74. package/src/usage/usage-plugin.ts +296 -0
  75. package/src/webhook/__tests__/webhook-plugin.test.ts +560 -0
  76. package/src/webhook/http-client.ts +141 -0
  77. package/src/webhook/index.ts +9 -0
  78. package/src/webhook/transformer.ts +209 -0
  79. package/src/webhook/types.ts +201 -0
  80. package/src/webhook/webhook-helpers.ts +60 -0
  81. package/src/webhook/webhook-plugin.ts +298 -0
  82. package/src/webhook/webhook-queue.ts +148 -0
@@ -0,0 +1,224 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { describe, it, expect } from 'vitest';
3
+ import { ExecutionAnalyticsPlugin } from '../execution-analytics-plugin';
4
+ import type { TUniversalMessage } from '@robota-sdk/agent-core';
5
+
6
+ function userMsg(content: string): TUniversalMessage {
7
+ return {
8
+ id: randomUUID(),
9
+ role: 'user',
10
+ content,
11
+ state: 'complete',
12
+ timestamp: new Date(),
13
+ };
14
+ }
15
+
16
+ function assistantMsg(content: string): TUniversalMessage {
17
+ return {
18
+ id: randomUUID(),
19
+ role: 'assistant',
20
+ content,
21
+ state: 'complete',
22
+ timestamp: new Date(),
23
+ };
24
+ }
25
+
26
+ describe('ExecutionAnalyticsPlugin', () => {
27
+ describe('constructor', () => {
28
+ it('initializes with default options', () => {
29
+ const plugin = new ExecutionAnalyticsPlugin();
30
+ expect(plugin.name).toBe('ExecutionAnalyticsPlugin');
31
+ expect(plugin.version).toBe('1.0.0');
32
+ });
33
+
34
+ it('initializes with custom options', () => {
35
+ const plugin = new ExecutionAnalyticsPlugin({
36
+ maxEntries: 500,
37
+ performanceThreshold: 2000,
38
+ trackErrors: false,
39
+ });
40
+ expect(plugin.getPluginStats().totalRecorded).toBe(0);
41
+ });
42
+ });
43
+
44
+ describe('run tracking', () => {
45
+ it('tracks beforeRun and afterRun lifecycle', async () => {
46
+ const plugin = new ExecutionAnalyticsPlugin();
47
+
48
+ await plugin.beforeRun('hello world');
49
+ expect(plugin.getActiveExecutions()).toHaveLength(1);
50
+ expect(plugin.getActiveExecutions()[0].operation).toBe('run');
51
+
52
+ await plugin.afterRun('hello world', 'response text');
53
+ expect(plugin.getActiveExecutions()).toHaveLength(0);
54
+
55
+ const stats = plugin.getExecutionStats('run');
56
+ expect(stats).toHaveLength(1);
57
+ expect(stats[0].success).toBe(true);
58
+ expect(stats[0].operation).toBe('run');
59
+ expect(stats[0].duration).toBeGreaterThanOrEqual(0);
60
+ });
61
+
62
+ it('records metadata with input/output lengths', async () => {
63
+ const plugin = new ExecutionAnalyticsPlugin();
64
+
65
+ await plugin.beforeRun('input text');
66
+ await plugin.afterRun('input text', 'output text');
67
+
68
+ const stats = plugin.getExecutionStats('run');
69
+ expect(stats[0].metadata?.inputLength).toBe(10);
70
+ expect(stats[0].metadata?.responseLength).toBe(11);
71
+ });
72
+ });
73
+
74
+ describe('provider call tracking', () => {
75
+ it('tracks provider call lifecycle', async () => {
76
+ const plugin = new ExecutionAnalyticsPlugin();
77
+ const messages = [userMsg('what is 1+1?')];
78
+ const response = assistantMsg('2');
79
+
80
+ await plugin.beforeProviderCall(messages);
81
+ expect(plugin.getActiveExecutions()).toHaveLength(1);
82
+
83
+ await plugin.afterProviderCall(messages, response);
84
+ expect(plugin.getActiveExecutions()).toHaveLength(0);
85
+
86
+ const stats = plugin.getExecutionStats('provider-call');
87
+ expect(stats).toHaveLength(1);
88
+ expect(stats[0].success).toBe(true);
89
+ });
90
+ });
91
+
92
+ describe('tool call tracking', () => {
93
+ it('tracks tool call lifecycle', async () => {
94
+ const plugin = new ExecutionAnalyticsPlugin();
95
+
96
+ await plugin.beforeToolCall('calculator', { a: 1, b: 2 });
97
+ await plugin.afterToolCall('calculator', { a: 1, b: 2 }, { success: true, result: '3' });
98
+
99
+ const stats = plugin.getExecutionStats('tool-call');
100
+ expect(stats).toHaveLength(1);
101
+ expect(stats[0].success).toBe(true);
102
+ expect(stats[0].metadata?.toolName).toBe('calculator');
103
+ });
104
+
105
+ it('tracks tool call with error', async () => {
106
+ const plugin = new ExecutionAnalyticsPlugin({ trackErrors: true });
107
+
108
+ await plugin.beforeToolCall('failing-tool', {});
109
+ await plugin.afterToolCall('failing-tool', {}, { success: false, error: 'tool failed' });
110
+
111
+ const stats = plugin.getExecutionStats('tool-call');
112
+ expect(stats).toHaveLength(1);
113
+ expect(stats[0].success).toBe(false);
114
+ expect(stats[0].error?.type).toBe('ToolExecutionError');
115
+ });
116
+ });
117
+
118
+ describe('error tracking', () => {
119
+ it('records error for active execution', async () => {
120
+ const plugin = new ExecutionAnalyticsPlugin();
121
+
122
+ await plugin.beforeRun('test input');
123
+ await plugin.onError(new Error('something went wrong'));
124
+
125
+ const stats = plugin.getExecutionStats();
126
+ expect(stats).toHaveLength(1);
127
+ expect(stats[0].success).toBe(false);
128
+ expect(stats[0].error?.message).toBe('something went wrong');
129
+ });
130
+
131
+ it('handles error when no active execution exists', async () => {
132
+ const plugin = new ExecutionAnalyticsPlugin();
133
+ await expect(plugin.onError(new Error('orphan error'))).resolves.not.toThrow();
134
+ });
135
+ });
136
+
137
+ describe('getAggregatedStats', () => {
138
+ it('returns empty stats when no executions recorded', () => {
139
+ const plugin = new ExecutionAnalyticsPlugin();
140
+ const aggregated = plugin.getAggregatedStats();
141
+
142
+ expect(aggregated.totalExecutions).toBe(0);
143
+ expect(aggregated.successRate).toBe(0);
144
+ expect(aggregated.averageDuration).toBe(0);
145
+ });
146
+
147
+ it('aggregates across multiple operations', async () => {
148
+ const plugin = new ExecutionAnalyticsPlugin();
149
+
150
+ await plugin.beforeRun('input1');
151
+ await plugin.afterRun('input1', 'output1');
152
+
153
+ await plugin.beforeRun('input2');
154
+ await plugin.afterRun('input2', 'output2');
155
+
156
+ await plugin.beforeToolCall('tool1', {});
157
+ await plugin.afterToolCall('tool1', {}, { success: false, error: 'fail' });
158
+
159
+ const aggregated = plugin.getAggregatedStats();
160
+ expect(aggregated.totalExecutions).toBe(3);
161
+ expect(aggregated.successfulExecutions).toBe(2);
162
+ expect(aggregated.failedExecutions).toBe(1);
163
+ expect(aggregated.operationStats['run']?.count).toBe(2);
164
+ expect(aggregated.operationStats['tool-call']?.count).toBe(1);
165
+ });
166
+ });
167
+
168
+ describe('max entries rotation', () => {
169
+ it('maintains max entries limit by removing oldest', async () => {
170
+ const plugin = new ExecutionAnalyticsPlugin({ maxEntries: 3 });
171
+
172
+ for (let i = 0; i < 5; i++) {
173
+ await plugin.beforeRun(`input-${i}`);
174
+ await plugin.afterRun(`input-${i}`, `output-${i}`);
175
+ }
176
+
177
+ const stats = plugin.getExecutionStats();
178
+ expect(stats).toHaveLength(3);
179
+ });
180
+ });
181
+
182
+ describe('clearStats', () => {
183
+ it('clears all execution data', async () => {
184
+ const plugin = new ExecutionAnalyticsPlugin();
185
+
186
+ await plugin.beforeRun('input');
187
+ await plugin.afterRun('input', 'output');
188
+
189
+ plugin.clearStats();
190
+
191
+ expect(plugin.getExecutionStats()).toHaveLength(0);
192
+ expect(plugin.getActiveExecutions()).toHaveLength(0);
193
+ expect(plugin.getPluginStats().totalRecorded).toBe(0);
194
+ });
195
+ });
196
+
197
+ describe('getPluginStats', () => {
198
+ it('reports memory usage and record count', async () => {
199
+ const plugin = new ExecutionAnalyticsPlugin();
200
+
201
+ await plugin.beforeRun('test');
202
+ await plugin.afterRun('test', 'response');
203
+
204
+ const pluginStats = plugin.getPluginStats();
205
+ expect(pluginStats.totalRecorded).toBe(1);
206
+ expect(pluginStats.activeExecutions).toBe(0);
207
+ expect(pluginStats.oldestRecord).toBeDefined();
208
+ expect(pluginStats.newestRecord).toBeDefined();
209
+ });
210
+ });
211
+
212
+ describe('destroy', () => {
213
+ it('clears all data on destroy', async () => {
214
+ const plugin = new ExecutionAnalyticsPlugin();
215
+
216
+ await plugin.beforeRun('test');
217
+ await plugin.afterRun('test', 'response');
218
+
219
+ await plugin.destroy();
220
+
221
+ expect(plugin.getExecutionStats()).toHaveLength(0);
222
+ });
223
+ });
224
+ });
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Stats aggregation logic for ExecutionAnalyticsPlugin.
3
+ *
4
+ * Extracted from execution-analytics-plugin.ts to keep each file under 300 lines.
5
+ * @internal
6
+ */
7
+ import type { IExecutionStats, IAggregatedExecutionStats } from './types';
8
+
9
+ /** Compute aggregated analytics across recorded executions. @internal */
10
+ export function aggregateExecutionStats(
11
+ stats: IExecutionStats[],
12
+ timeRange?: { start: Date; end: Date },
13
+ ): IAggregatedExecutionStats {
14
+ if (stats.length === 0) {
15
+ return {
16
+ totalExecutions: 0,
17
+ successfulExecutions: 0,
18
+ failedExecutions: 0,
19
+ successRate: 0,
20
+ averageDuration: 0,
21
+ totalDuration: 0,
22
+ operationStats: {},
23
+ errorStats: {},
24
+ timeRange: timeRange || { start: new Date(), end: new Date() },
25
+ };
26
+ }
27
+
28
+ const totalExecutions = stats.length;
29
+ const successfulExecutions = stats.filter((s) => s.success).length;
30
+ const failedExecutions = totalExecutions - successfulExecutions;
31
+ const totalDuration = stats.reduce((sum, s) => sum + s.duration, 0);
32
+ const averageDuration = totalDuration / totalExecutions;
33
+
34
+ const operationStats: Record<
35
+ string,
36
+ {
37
+ count: number;
38
+ successCount: number;
39
+ failureCount: number;
40
+ totalDuration: number;
41
+ averageDuration: number;
42
+ }
43
+ > = {};
44
+
45
+ for (const stat of stats) {
46
+ if (!operationStats[stat.operation]) {
47
+ operationStats[stat.operation] = {
48
+ count: 0,
49
+ successCount: 0,
50
+ failureCount: 0,
51
+ totalDuration: 0,
52
+ averageDuration: 0,
53
+ };
54
+ }
55
+ const opStat = operationStats[stat.operation];
56
+ if (opStat) {
57
+ opStat.count++;
58
+ opStat.totalDuration += stat.duration;
59
+ if (stat.success) opStat.successCount++;
60
+ else opStat.failureCount++;
61
+ }
62
+ }
63
+ for (const op in operationStats) {
64
+ const opStat = operationStats[op];
65
+ if (opStat && opStat.count > 0) opStat.averageDuration = opStat.totalDuration / opStat.count;
66
+ }
67
+
68
+ const errorStats: Record<string, number> = {};
69
+ for (const stat of stats.filter((s) => !s.success && s.error)) {
70
+ const errorType = stat.error!.type;
71
+ errorStats[errorType] = (errorStats[errorType] || 0) + 1;
72
+ }
73
+
74
+ return {
75
+ totalExecutions,
76
+ successfulExecutions,
77
+ failedExecutions,
78
+ successRate: totalExecutions > 0 ? successfulExecutions / totalExecutions : 0,
79
+ averageDuration,
80
+ totalDuration,
81
+ operationStats,
82
+ errorStats,
83
+ timeRange: timeRange || {
84
+ start: stats[0]?.startTime || new Date(),
85
+ end: stats[stats.length - 1]?.endTime || new Date(),
86
+ },
87
+ };
88
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Execution Analytics Plugin - Validation and utility helpers.
3
+ *
4
+ * Extracted from execution-analytics-plugin.ts to keep each file under 300 lines.
5
+ * @internal
6
+ */
7
+
8
+ import type { IExecutionAnalyticsOptions, IExecutionStats } from './types';
9
+
10
+ /**
11
+ * Validate and sanitize ExecutionAnalyticsPlugin options in-place.
12
+ * Invalid numeric values are reset to safe defaults. @internal
13
+ */
14
+ export function validateExecutionAnalyticsOptions(
15
+ options: IExecutionAnalyticsOptions,
16
+ pluginOptions: { maxEntries: number; performanceThreshold: number },
17
+ ): void {
18
+ if (options.maxEntries !== undefined && options.maxEntries < 1) {
19
+ pluginOptions.maxEntries = 1000;
20
+ }
21
+ if (options.performanceThreshold !== undefined && options.performanceThreshold < 0) {
22
+ pluginOptions.performanceThreshold = 5000;
23
+ }
24
+ }
25
+
26
+ /** Generate a unique execution ID with an optional prefix. @internal */
27
+ export function generateExecutionId(prefix: string, counter: number): string {
28
+ return `${prefix}-${Date.now()}-${counter}`;
29
+ }
30
+
31
+ /**
32
+ * Build an IExecutionStats record for an error event. @internal
33
+ */
34
+ export function buildErrorExecutionStats(
35
+ executionId: string,
36
+ executionData: { startTime: number; operation: string },
37
+ error: Error,
38
+ trackErrors: boolean,
39
+ context?: { action?: string; tool?: string; attempt?: number },
40
+ ): IExecutionStats {
41
+ const duration = Date.now() - executionData.startTime;
42
+ const errorInfo = trackErrors
43
+ ? {
44
+ message: error.message,
45
+ ...(error.stack && { stack: error.stack }),
46
+ type: error.constructor.name,
47
+ }
48
+ : undefined;
49
+ return {
50
+ executionId,
51
+ operation: executionData.operation,
52
+ startTime: new Date(executionData.startTime),
53
+ endTime: new Date(),
54
+ duration,
55
+ success: false,
56
+ ...(errorInfo && { error: errorInfo }),
57
+ metadata: {
58
+ errorSource: 'onError-hook',
59
+ contextType: context ? typeof context : 'none',
60
+ hasContext: !!context,
61
+ },
62
+ };
63
+ }
64
+
65
+ /** Find the first active execution matching the given operation (and optionally the input). @internal */
66
+ export function findActiveExecution(
67
+ activeExecutions: Map<string, { startTime: number; operation: string; input?: string }>,
68
+ operation: string,
69
+ input?: string,
70
+ ):
71
+ | {
72
+ executionId: string;
73
+ executionData: { startTime: number; operation: string; input?: string };
74
+ }
75
+ | undefined {
76
+ for (const [executionId, executionData] of activeExecutions.entries()) {
77
+ if (executionData.operation === operation) {
78
+ if (operation === 'run' && input && executionData.input !== input) continue;
79
+ return { executionId, executionData };
80
+ }
81
+ }
82
+ return undefined;
83
+ }
@@ -0,0 +1,315 @@
1
+ import {
2
+ AbstractPlugin,
3
+ PluginCategory,
4
+ PluginPriority,
5
+ type IPluginErrorContext,
6
+ createLogger,
7
+ type ILogger,
8
+ type IRunOptions,
9
+ type TUniversalMessage,
10
+ isAssistantMessage,
11
+ type TToolParameters,
12
+ type IToolExecutionResult,
13
+ } from '@robota-sdk/agent-core';
14
+ import type {
15
+ IExecutionStats,
16
+ IAggregatedExecutionStats,
17
+ IExecutionAnalyticsOptions,
18
+ IExecutionAnalyticsPluginStats,
19
+ } from './types';
20
+ import { aggregateExecutionStats } from './analytics-aggregation';
21
+ import {
22
+ validateExecutionAnalyticsOptions,
23
+ generateExecutionId,
24
+ findActiveExecution,
25
+ buildErrorExecutionStats,
26
+ } from './execution-analytics-helpers';
27
+
28
+ const DEFAULT_MAX_ENTRIES = 1000;
29
+ const DEFAULT_PERFORMANCE_THRESHOLD_MS = 5000;
30
+ const PREVIEW_LENGTH = 100;
31
+
32
+ /**
33
+ * Tracks timing and success/failure of agent runs, provider calls, and tool executions.
34
+ * @extends AbstractPlugin
35
+ */
36
+ export class ExecutionAnalyticsPlugin extends AbstractPlugin<
37
+ IExecutionAnalyticsOptions,
38
+ IExecutionAnalyticsPluginStats
39
+ > {
40
+ name = 'ExecutionAnalyticsPlugin';
41
+ version = '1.0.0';
42
+
43
+ private pluginOptions: Required<IExecutionAnalyticsOptions>;
44
+ private logger: ILogger;
45
+ private activeExecutions: Map<string, { startTime: number; operation: string; input?: string }> =
46
+ new Map();
47
+ private executionHistory: IExecutionStats[] = [];
48
+ private executionCounter = 0;
49
+ private initialized = false;
50
+
51
+ constructor(options: IExecutionAnalyticsOptions = {}) {
52
+ super();
53
+ this.category = PluginCategory.MONITORING;
54
+ this.priority = PluginPriority.NORMAL;
55
+ this.pluginOptions = {
56
+ enabled: options.enabled ?? true,
57
+ maxEntries: options.maxEntries || DEFAULT_MAX_ENTRIES,
58
+ trackErrors: options.trackErrors ?? true,
59
+ performanceThreshold: options.performanceThreshold || DEFAULT_PERFORMANCE_THRESHOLD_MS,
60
+ enableWarnings: options.enableWarnings ?? true,
61
+ category: options.category ?? PluginCategory.MONITORING,
62
+ priority: options.priority ?? PluginPriority.NORMAL,
63
+ moduleEvents: options.moduleEvents ?? [],
64
+ subscribeToAllModuleEvents: options.subscribeToAllModuleEvents ?? false,
65
+ };
66
+ validateExecutionAnalyticsOptions(options, this.pluginOptions);
67
+ this.logger = createLogger('ExecutionAnalyticsPlugin');
68
+ this.beforeRun = this.beforeRun.bind(this);
69
+ this.afterRun = this.afterRun.bind(this);
70
+ this.beforeProviderCall = this.beforeProviderCall.bind(this);
71
+ this.afterProviderCall = this.afterProviderCall.bind(this);
72
+ this.initialized = true;
73
+ }
74
+
75
+ override beforeRun = async (input: string, _options?: IRunOptions): Promise<void> => {
76
+ this.activeExecutions.set(generateExecutionId('exec', ++this.executionCounter), {
77
+ startTime: Date.now(),
78
+ operation: 'run',
79
+ input: input.substring(0, PREVIEW_LENGTH),
80
+ });
81
+ };
82
+
83
+ override afterRun = async (
84
+ input: string,
85
+ response: string,
86
+ options?: IRunOptions,
87
+ ): Promise<void> => {
88
+ const execution = findActiveExecution(this.activeExecutions, 'run', input);
89
+ if (!execution) return;
90
+ const { executionId, executionData } = execution;
91
+ const duration = Date.now() - executionData.startTime;
92
+ this.recordStats({
93
+ executionId,
94
+ operation: 'run',
95
+ startTime: new Date(executionData.startTime),
96
+ endTime: new Date(),
97
+ duration,
98
+ success: true,
99
+ metadata: {
100
+ inputLength: input.length,
101
+ responseLength: response.length,
102
+ hasOptions: !!options,
103
+ modelName: String(options?.metadata?.['model'] || 'unknown'),
104
+ },
105
+ });
106
+ this.activeExecutions.delete(executionId);
107
+ if (this.pluginOptions.enableWarnings && duration > this.pluginOptions.performanceThreshold) {
108
+ this.logger.warn('Slow run execution detected', {
109
+ executionId,
110
+ duration,
111
+ threshold: this.pluginOptions.performanceThreshold,
112
+ });
113
+ }
114
+ };
115
+
116
+ override beforeProviderCall = async (messages: TUniversalMessage[]): Promise<void> => {
117
+ this.activeExecutions.set(generateExecutionId('provider', ++this.executionCounter), {
118
+ startTime: Date.now(),
119
+ operation: 'provider-call',
120
+ input: messages[0]?.content || 'N/A',
121
+ });
122
+ };
123
+
124
+ override afterProviderCall = async (
125
+ messages: TUniversalMessage[],
126
+ response: TUniversalMessage,
127
+ ): Promise<void> => {
128
+ const execution = findActiveExecution(
129
+ this.activeExecutions,
130
+ 'provider-call',
131
+ messages[0]?.content || '',
132
+ );
133
+ if (!execution) return;
134
+ const { executionId, executionData } = execution;
135
+ const duration = Date.now() - executionData.startTime;
136
+ this.recordStats({
137
+ executionId,
138
+ operation: 'provider-call',
139
+ startTime: new Date(executionData.startTime),
140
+ endTime: new Date(),
141
+ duration,
142
+ success: true,
143
+ metadata: {
144
+ inputLength: messages[0]?.content?.length || 0,
145
+ responseLength: response.content?.length || 0,
146
+ hasToolCalls: !!(
147
+ isAssistantMessage(response) &&
148
+ response.toolCalls &&
149
+ response.toolCalls.length > 0
150
+ ),
151
+ toolCallCount:
152
+ isAssistantMessage(response) && response.toolCalls ? response.toolCalls.length : 0,
153
+ },
154
+ });
155
+ this.activeExecutions.delete(executionId);
156
+ if (this.pluginOptions.enableWarnings && duration > this.pluginOptions.performanceThreshold) {
157
+ this.logger.warn('Slow provider call detected', {
158
+ executionId,
159
+ duration,
160
+ threshold: this.pluginOptions.performanceThreshold,
161
+ });
162
+ }
163
+ };
164
+
165
+ override async beforeToolCall(toolName: string, _parameters: TToolParameters): Promise<void> {
166
+ this.activeExecutions.set(generateExecutionId('tool', ++this.executionCounter), {
167
+ startTime: Date.now(),
168
+ operation: 'tool-call',
169
+ input: toolName,
170
+ });
171
+ }
172
+
173
+ override async afterToolCall(
174
+ toolName: string,
175
+ parameters: TToolParameters,
176
+ result: IToolExecutionResult,
177
+ ): Promise<void> {
178
+ const execution = findActiveExecution(this.activeExecutions, 'tool-call', toolName);
179
+ if (!execution) return;
180
+ const { executionId, executionData } = execution;
181
+ const duration = Date.now() - executionData.startTime;
182
+ const success = !result?.error;
183
+ const errorInfo =
184
+ result?.error && this.pluginOptions.trackErrors
185
+ ? { message: String(result.error), type: 'ToolExecutionError' }
186
+ : undefined;
187
+ this.recordStats({
188
+ executionId,
189
+ operation: 'tool-call',
190
+ startTime: new Date(executionData.startTime),
191
+ endTime: new Date(),
192
+ duration,
193
+ success,
194
+ ...(errorInfo && { error: errorInfo }),
195
+ metadata: {
196
+ toolName,
197
+ parameterCount: Object.keys(parameters).length,
198
+ resultType: typeof result,
199
+ hasError: !!result?.error,
200
+ },
201
+ });
202
+ this.activeExecutions.delete(executionId);
203
+ }
204
+
205
+ override async onError(error: Error, context?: IPluginErrorContext): Promise<void> {
206
+ const activeExecution = Array.from(this.activeExecutions.entries())[0];
207
+ if (activeExecution) {
208
+ const [executionId, executionData] = activeExecution;
209
+ this.recordStats(
210
+ buildErrorExecutionStats(
211
+ executionId,
212
+ executionData,
213
+ error,
214
+ this.pluginOptions.trackErrors,
215
+ context,
216
+ ),
217
+ );
218
+ this.activeExecutions.delete(executionId);
219
+ }
220
+ }
221
+
222
+ getExecutionStats(operation?: string, timeRange?: { start: Date; end: Date }): IExecutionStats[] {
223
+ let filtered = this.executionHistory;
224
+ if (operation) filtered = filtered.filter((stat) => stat.operation === operation);
225
+ if (timeRange)
226
+ filtered = filtered.filter(
227
+ (stat) => stat.startTime >= timeRange.start && stat.startTime <= timeRange.end,
228
+ );
229
+ return [...filtered];
230
+ }
231
+
232
+ getAggregatedStats(timeRange?: { start: Date; end: Date }): IAggregatedExecutionStats {
233
+ return aggregateExecutionStats(this.getExecutionStats(undefined, timeRange), timeRange);
234
+ }
235
+
236
+ clearStats(): void {
237
+ this.executionHistory = [];
238
+ this.activeExecutions.clear();
239
+ this.executionCounter = 0;
240
+ }
241
+
242
+ getActiveExecutions(): Array<{ executionId: string; operation: string; duration: number }> {
243
+ const now = Date.now();
244
+ return Array.from(this.activeExecutions.entries()).map(([executionId, data]) => ({
245
+ executionId,
246
+ operation: data.operation,
247
+ duration: now - data.startTime,
248
+ }));
249
+ }
250
+
251
+ getPluginStats(): {
252
+ totalRecorded: number;
253
+ activeExecutions: number;
254
+ memoryUsage: number;
255
+ oldestRecord?: Date;
256
+ newestRecord?: Date;
257
+ } {
258
+ const oldest = this.executionHistory[0];
259
+ const newest = this.executionHistory[this.executionHistory.length - 1];
260
+ return {
261
+ totalRecorded: this.executionHistory.length,
262
+ activeExecutions: this.activeExecutions.size,
263
+ memoryUsage: this.executionHistory.length + this.activeExecutions.size,
264
+ ...(oldest && { oldestRecord: oldest.startTime }),
265
+ ...(newest && { newestRecord: newest.endTime }),
266
+ };
267
+ }
268
+
269
+ async destroy(): Promise<void> {
270
+ this.clearStats();
271
+ }
272
+ getExecutionData(): IExecutionStats[] {
273
+ return [...this.executionHistory];
274
+ }
275
+ getAnalyticsStats(): IAggregatedExecutionStats {
276
+ return this.getAggregatedStats();
277
+ }
278
+ clearExecutionData(): void {
279
+ this.clearStats();
280
+ }
281
+
282
+ override getStatus() {
283
+ return {
284
+ name: this.name,
285
+ version: this.version,
286
+ enabled: this.enabled,
287
+ initialized: this.initialized,
288
+ category: this.category,
289
+ priority: this.priority,
290
+ subscribedEventsCount: this.subscribedEvents.length,
291
+ hasEventEmitter: !!this.eventEmitter,
292
+ };
293
+ }
294
+
295
+ override getStats(): IExecutionAnalyticsPluginStats {
296
+ const newest = this.executionHistory[this.executionHistory.length - 1];
297
+ const oldest = this.executionHistory[0];
298
+ return {
299
+ enabled: this.enabled,
300
+ calls: this.executionHistory.length,
301
+ errors: this.executionHistory.filter((e) => !e.success).length,
302
+ ...(newest?.endTime && { lastActivity: newest.endTime }),
303
+ totalRecorded: this.executionHistory.length,
304
+ activeExecutions: this.activeExecutions.size,
305
+ memoryUsage: this.executionHistory.length + this.activeExecutions.size,
306
+ ...(oldest && { oldestRecord: oldest.startTime }),
307
+ ...(newest && { newestRecord: newest.endTime }),
308
+ };
309
+ }
310
+
311
+ private recordStats(stats: IExecutionStats): void {
312
+ this.executionHistory.push(stats);
313
+ if (this.executionHistory.length > this.pluginOptions.maxEntries) this.executionHistory.shift();
314
+ }
315
+ }