@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,116 @@
1
+ import {
2
+ PluginCategory,
3
+ PluginPriority,
4
+ ConfigurationError,
5
+ type TEventName,
6
+ EVENT_EMITTER_EVENTS,
7
+ } from '@robota-sdk/agent-core';
8
+ import type { IUsagePluginOptions } from './types';
9
+
10
+ const DEFAULT_MAX_ENTRIES = 10000;
11
+ const DEFAULT_BATCH_SIZE = 50;
12
+ const DEFAULT_FLUSH_INTERVAL_MS = 60000;
13
+ const DEFAULT_AGGREGATION_INTERVAL_MS = 300000;
14
+
15
+ export type TResolvedUsageOptions = Required<Omit<IUsagePluginOptions, 'costRates'>> & {
16
+ costRates?: Record<string, { input: number; output: number }>;
17
+ };
18
+
19
+ export function resolvePluginOptions(options: IUsagePluginOptions): TResolvedUsageOptions {
20
+ return {
21
+ enabled: options.enabled ?? true,
22
+ strategy: options.strategy,
23
+ filePath: options.filePath ?? './usage-stats.json',
24
+ remoteEndpoint: options.remoteEndpoint ?? '',
25
+ remoteHeaders: options.remoteHeaders ?? {},
26
+ maxEntries: options.maxEntries ?? DEFAULT_MAX_ENTRIES,
27
+ trackCosts: options.trackCosts ?? true,
28
+ ...(options.costRates && { costRates: options.costRates }),
29
+ batchSize: options.batchSize ?? DEFAULT_BATCH_SIZE,
30
+ flushInterval: options.flushInterval ?? DEFAULT_FLUSH_INTERVAL_MS,
31
+ aggregateStats: options.aggregateStats ?? true,
32
+ aggregationInterval: options.aggregationInterval ?? DEFAULT_AGGREGATION_INTERVAL_MS,
33
+ category: options.category ?? PluginCategory.MONITORING,
34
+ priority: options.priority ?? PluginPriority.NORMAL,
35
+ moduleEvents: options.moduleEvents ?? [],
36
+ subscribeToAllModuleEvents: options.subscribeToAllModuleEvents ?? false,
37
+ };
38
+ }
39
+
40
+ export function calculateCost(
41
+ costRates: Record<string, { input: number; output: number }> | undefined,
42
+ model: string,
43
+ tokens: { input: number; output: number },
44
+ ): { input: number; output: number; total: number } | undefined {
45
+ if (!costRates || !costRates[model]) return undefined;
46
+ const rates = costRates[model];
47
+ const inputCost = tokens.input * rates.input;
48
+ const outputCost = tokens.output * rates.output;
49
+ return { input: inputCost, output: outputCost, total: inputCost + outputCost };
50
+ }
51
+
52
+ const VALID_STRATEGIES = ['memory', 'file', 'remote', 'silent'] as const;
53
+
54
+ export function validateUsageOptions(options: IUsagePluginOptions): void {
55
+ if (!options.strategy) {
56
+ throw new ConfigurationError('Usage tracking strategy is required');
57
+ }
58
+ if (!(VALID_STRATEGIES as readonly string[]).includes(options.strategy)) {
59
+ throw new ConfigurationError('Invalid usage tracking strategy', {
60
+ validStrategies: [...VALID_STRATEGIES],
61
+ provided: options.strategy,
62
+ });
63
+ }
64
+ if (options.strategy === 'file' && !options.filePath) {
65
+ throw new ConfigurationError('File path is required for file usage tracking strategy');
66
+ }
67
+ if (options.strategy === 'remote' && !options.remoteEndpoint) {
68
+ throw new ConfigurationError('Remote endpoint is required for remote usage tracking strategy');
69
+ }
70
+ if (options.maxEntries !== undefined && options.maxEntries <= 0) {
71
+ throw new ConfigurationError('Max entries must be positive');
72
+ }
73
+ if (options.batchSize !== undefined && options.batchSize <= 0) {
74
+ throw new ConfigurationError('Batch size must be positive');
75
+ }
76
+ if (options.flushInterval !== undefined && options.flushInterval <= 0) {
77
+ throw new ConfigurationError('Flush interval must be positive');
78
+ }
79
+ if (options.aggregationInterval !== undefined && options.aggregationInterval <= 0) {
80
+ throw new ConfigurationError('Aggregation interval must be positive');
81
+ }
82
+ }
83
+
84
+ const MODULE_SUCCESS_EVENTS = new Set<TEventName>([
85
+ EVENT_EMITTER_EVENTS.MODULE_INITIALIZE_COMPLETE,
86
+ EVENT_EMITTER_EVENTS.MODULE_EXECUTION_COMPLETE,
87
+ EVENT_EMITTER_EVENTS.MODULE_DISPOSE_COMPLETE,
88
+ ]);
89
+
90
+ const MODULE_ERROR_EVENTS = new Set<TEventName>([
91
+ EVENT_EMITTER_EVENTS.MODULE_INITIALIZE_ERROR,
92
+ EVENT_EMITTER_EVENTS.MODULE_EXECUTION_ERROR,
93
+ EVENT_EMITTER_EVENTS.MODULE_DISPOSE_ERROR,
94
+ ]);
95
+
96
+ export function isModuleSuccessEvent(eventName: TEventName): boolean {
97
+ return MODULE_SUCCESS_EVENTS.has(eventName);
98
+ }
99
+
100
+ export function isModuleErrorEvent(eventName: TEventName): boolean {
101
+ return MODULE_ERROR_EVENTS.has(eventName);
102
+ }
103
+
104
+ export function extractStringField(data: unknown, field: string): string {
105
+ if (data && typeof data === 'object' && field in data) {
106
+ const value = (data as Record<string, unknown>)[field];
107
+ if (typeof value === 'string') return value;
108
+ }
109
+ return 'unknown';
110
+ }
111
+
112
+ export function resolveOperation(eventName: TEventName): string {
113
+ if (eventName.includes('initialize')) return 'initialization';
114
+ if (eventName.includes('execution')) return 'execution';
115
+ return 'disposal';
116
+ }
@@ -0,0 +1,296 @@
1
+ import {
2
+ AbstractPlugin,
3
+ PluginCategory,
4
+ PluginPriority,
5
+ createLogger,
6
+ type ILogger,
7
+ PluginError,
8
+ ConfigurationError,
9
+ type IEventEmitterEventData,
10
+ type TEventName,
11
+ type TTimerId,
12
+ startPeriodicTask,
13
+ } from '@robota-sdk/agent-core';
14
+ import {
15
+ IUsageStats,
16
+ IAggregatedUsageStats,
17
+ IUsagePluginOptions,
18
+ IUsagePluginStats,
19
+ IUsageStorage,
20
+ } from './types';
21
+ import {
22
+ MemoryUsageStorage,
23
+ FileUsageStorage,
24
+ RemoteUsageStorage,
25
+ SilentUsageStorage,
26
+ } from './storages/index';
27
+ import {
28
+ type TResolvedUsageOptions,
29
+ resolvePluginOptions,
30
+ validateUsageOptions,
31
+ calculateCost,
32
+ isModuleSuccessEvent,
33
+ isModuleErrorEvent,
34
+ extractStringField,
35
+ resolveOperation,
36
+ } from './usage-plugin-helpers';
37
+
38
+ const DEFAULT_REMOTE_TIMEOUT_MS = 30000;
39
+
40
+ /**
41
+ * Tracks token usage, request counts, and costs across agent executions.
42
+ *
43
+ * Supports memory, file, remote, and silent storage strategies. When
44
+ * {@link IUsagePluginOptions.trackCosts | trackCosts} is enabled, per-model
45
+ * cost rates are applied automatically. Periodic aggregation can be enabled
46
+ * via {@link IUsagePluginOptions.aggregateStats | aggregateStats}.
47
+ *
48
+ * Lifecycle hooks used: {@link AbstractPlugin.onModuleEvent | onModuleEvent}
49
+ *
50
+ * @extends AbstractPlugin
51
+ * @see IUsageStorage - storage backend contract
52
+ * @see IUsagePluginOptions - configuration options
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * const plugin = new UsagePlugin({
57
+ * strategy: 'memory',
58
+ * trackCosts: true,
59
+ * costRates: { 'gpt-4': { input: 0.03, output: 0.06 } },
60
+ * });
61
+ * await plugin.recordUsage({ provider: 'openai', model: 'gpt-4', ... });
62
+ * ```
63
+ */
64
+ export class UsagePlugin extends AbstractPlugin<IUsagePluginOptions, IUsagePluginStats> {
65
+ name = 'UsagePlugin';
66
+ version = '1.0.0';
67
+
68
+ private storage: IUsageStorage;
69
+ private pluginOptions: TResolvedUsageOptions;
70
+ private logger: ILogger;
71
+ private aggregationTimer?: TTimerId;
72
+
73
+ constructor(options: IUsagePluginOptions) {
74
+ super();
75
+ this.logger = createLogger('UsagePlugin');
76
+ this.category = PluginCategory.MONITORING;
77
+ this.priority = PluginPriority.NORMAL;
78
+
79
+ validateUsageOptions(options);
80
+ this.pluginOptions = resolvePluginOptions(options);
81
+ this.storage = this.createStorage();
82
+
83
+ if (this.pluginOptions.aggregateStats) {
84
+ this.setupAggregation();
85
+ }
86
+
87
+ this.logger.info('UsagePlugin initialized', {
88
+ strategy: this.pluginOptions.strategy,
89
+ trackCosts: this.pluginOptions.trackCosts,
90
+ maxEntries: this.pluginOptions.maxEntries,
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Records usage statistics from module lifecycle events (completion and
96
+ * error events). Duration-bearing events are recorded with zero token
97
+ * counts as module events do not involve LLM calls.
98
+ */
99
+ override async onModuleEvent(
100
+ eventName: TEventName,
101
+ eventData: IEventEmitterEventData,
102
+ ): Promise<void> {
103
+ try {
104
+ const isSuccess = isModuleSuccessEvent(eventName);
105
+ const isError = isModuleErrorEvent(eventName);
106
+ if (!isSuccess && !isError) return;
107
+
108
+ const moduleData = eventData.data;
109
+ if (!moduleData || !('duration' in moduleData) || typeof moduleData.duration !== 'number')
110
+ return;
111
+
112
+ await this.recordModuleUsage(eventName, eventData, moduleData.duration, isSuccess);
113
+ } catch {
114
+ // Swallow to avoid breaking module event processing
115
+ }
116
+ }
117
+
118
+ private async recordModuleUsage(
119
+ eventName: TEventName,
120
+ eventData: IEventEmitterEventData,
121
+ duration: number,
122
+ success: boolean,
123
+ ): Promise<void> {
124
+ const moduleData = eventData.data;
125
+ const moduleName = extractStringField(moduleData, 'moduleName');
126
+ const moduleType = extractStringField(moduleData, 'moduleType');
127
+ const operation = resolveOperation(eventName);
128
+
129
+ const metadata: Record<string, string> = { moduleName, moduleType, operation };
130
+ if (!success) {
131
+ metadata['error'] = eventData.error?.message || 'unknown error';
132
+ }
133
+
134
+ await this.recordUsage({
135
+ provider: 'module',
136
+ model: moduleType,
137
+ tokensUsed: { input: 0, output: 0, total: 0 },
138
+ requestCount: 1,
139
+ duration,
140
+ success,
141
+ ...(eventData.executionId && { executionId: eventData.executionId }),
142
+ ...(eventData.sessionId && { conversationId: eventData.sessionId }),
143
+ metadata,
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Records a usage entry, calculating cost if cost tracking is enabled and
149
+ * a rate is configured for the model.
150
+ * @throws PluginError if the storage write fails
151
+ */
152
+ async recordUsage(usage: Omit<IUsageStats, 'timestamp' | 'cost'>): Promise<void> {
153
+ try {
154
+ const cost = this.pluginOptions.trackCosts
155
+ ? calculateCost(this.pluginOptions.costRates, usage.model, usage.tokensUsed)
156
+ : undefined;
157
+
158
+ const entry: IUsageStats = {
159
+ ...usage,
160
+ timestamp: new Date(),
161
+ ...(cost && { cost }),
162
+ };
163
+
164
+ await this.storage.save(entry);
165
+
166
+ this.logger.debug('Usage recorded', {
167
+ provider: entry.provider,
168
+ model: entry.model,
169
+ tokens: entry.tokensUsed.total,
170
+ cost: entry.cost?.total,
171
+ success: entry.success,
172
+ });
173
+ } catch (error) {
174
+ throw new PluginError('Failed to record usage', this.name, {
175
+ error: error instanceof Error ? error.message : String(error),
176
+ });
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Retrieves usage entries, optionally filtered by conversation and time range.
182
+ * @throws PluginError if the storage read fails
183
+ */
184
+ async getUsageStats(
185
+ conversationId?: string,
186
+ timeRange?: { start: Date; end: Date },
187
+ ): Promise<IUsageStats[]> {
188
+ try {
189
+ return await this.storage.getStats(conversationId, timeRange);
190
+ } catch (error) {
191
+ throw new PluginError('Failed to get usage stats', this.name, {
192
+ conversationId: conversationId || 'all',
193
+ timeRange: timeRange
194
+ ? `${timeRange.start.toISOString()}-${timeRange.end.toISOString()}`
195
+ : 'all',
196
+ error: error instanceof Error ? error.message : String(error),
197
+ });
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Returns aggregated totals (requests, tokens, cost, success rate) across
203
+ * all recorded usage entries within the optional time range.
204
+ * @throws PluginError if the storage aggregation fails
205
+ */
206
+ async getAggregatedStats(timeRange?: { start: Date; end: Date }): Promise<IAggregatedUsageStats> {
207
+ try {
208
+ return await this.storage.getAggregatedStats(timeRange);
209
+ } catch (error) {
210
+ throw new PluginError('Failed to get aggregated usage stats', this.name, {
211
+ timeRange: timeRange
212
+ ? `${timeRange.start.toISOString()}-${timeRange.end.toISOString()}`
213
+ : 'all',
214
+ error: error instanceof Error ? error.message : String(error),
215
+ });
216
+ }
217
+ }
218
+
219
+ async clearStats(): Promise<void> {
220
+ try {
221
+ await this.storage.clear();
222
+ this.logger.info('Usage statistics cleared');
223
+ } catch (error) {
224
+ throw new PluginError('Failed to clear usage stats', this.name, {
225
+ error: error instanceof Error ? error.message : String(error),
226
+ });
227
+ }
228
+ }
229
+
230
+ async flush(): Promise<void> {
231
+ try {
232
+ await this.storage.flush();
233
+ } catch (error) {
234
+ throw new PluginError('Failed to flush usage stats', this.name, {
235
+ error: error instanceof Error ? error.message : String(error),
236
+ });
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Stops the aggregation timer and closes the underlying storage.
242
+ */
243
+ async destroy(): Promise<void> {
244
+ try {
245
+ if (this.aggregationTimer) {
246
+ clearInterval(this.aggregationTimer);
247
+ }
248
+ await this.storage.close();
249
+ this.logger.info('UsagePlugin destroyed');
250
+ } catch (error) {
251
+ this.logger.error('Error during plugin cleanup', {
252
+ error: error instanceof Error ? error.message : String(error),
253
+ });
254
+ }
255
+ }
256
+
257
+ private createStorage(): IUsageStorage {
258
+ switch (this.pluginOptions.strategy) {
259
+ case 'memory':
260
+ return new MemoryUsageStorage(this.pluginOptions.maxEntries);
261
+ case 'file':
262
+ return new FileUsageStorage(this.pluginOptions.filePath);
263
+ case 'remote':
264
+ return new RemoteUsageStorage(
265
+ this.pluginOptions.remoteEndpoint!,
266
+ '',
267
+ DEFAULT_REMOTE_TIMEOUT_MS,
268
+ this.pluginOptions.remoteHeaders || {},
269
+ this.pluginOptions.batchSize,
270
+ this.pluginOptions.flushInterval,
271
+ );
272
+ case 'silent':
273
+ return new SilentUsageStorage();
274
+ default:
275
+ throw new ConfigurationError('Unknown usage tracking strategy', {
276
+ strategy: this.pluginOptions.strategy,
277
+ });
278
+ }
279
+ }
280
+
281
+ private setupAggregation(): void {
282
+ this.aggregationTimer = startPeriodicTask(
283
+ this.logger,
284
+ { name: 'UsagePlugin.aggregate', intervalMs: this.pluginOptions.aggregationInterval },
285
+ async () => {
286
+ const stats = await this.getAggregatedStats();
287
+ this.logger.debug('Periodic usage aggregation', {
288
+ totalRequests: stats.totalRequests,
289
+ totalTokens: stats.totalTokens,
290
+ totalCost: stats.totalCost,
291
+ successRate: stats.successRate,
292
+ });
293
+ },
294
+ );
295
+ }
296
+ }