@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,9 @@
1
+ export { LimitsPlugin } from './limits-plugin';
2
+ export type {
3
+ TLimitsStrategy,
4
+ ILimitsPluginOptions,
5
+ TPluginLimitsStatusData,
6
+ ILimitWindow,
7
+ ITokenBucket,
8
+ } from './types';
9
+ export type { ILimitsPluginExecutionContext, ILimitsPluginExecutionResult } from './limits-plugin';
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Limits Plugin - Rate limit check, cost calculation, and utility helpers.
3
+ *
4
+ * Extracted from limits-plugin.ts to keep each file under 300 lines.
5
+ * @internal
6
+ */
7
+
8
+ import { PluginError } from '@robota-sdk/agent-core';
9
+ import type { TUniversalMessage } from '@robota-sdk/agent-core';
10
+ import type { ILimitWindow, ITokenBucket } from './types';
11
+
12
+ const COST_DECIMAL_PLACES = 4;
13
+ const CHARS_PER_TOKEN = 4;
14
+ const TOKEN_ESTIMATE_BUFFER = 100;
15
+ const TOKENS_PER_COST_UNIT = 1000;
16
+ const MS_PER_SECOND = 1000;
17
+
18
+ /** Model-specific cost table (per 1000 tokens). @internal */
19
+ const MODEL_COSTS: Record<string, number> = {
20
+ 'gpt-4': 0.03,
21
+ 'gpt-4-turbo': 0.01,
22
+ 'gpt-3.5-turbo': 0.002,
23
+ 'claude-3-opus': 0.015,
24
+ 'claude-3-sonnet': 0.003,
25
+ 'claude-3-haiku': 0.00025,
26
+ };
27
+
28
+ /** Default cost calculator: uses model-specific rates or falls back to tokenCostPer1000. @internal */
29
+ export function defaultCostCalculator(
30
+ tokens: number,
31
+ model: string,
32
+ tokenCostPer1000: number,
33
+ ): number {
34
+ return (tokens / TOKENS_PER_COST_UNIT) * (MODEL_COSTS[model] ?? tokenCostPer1000);
35
+ }
36
+
37
+ /** Estimate token count from message content lengths. @internal */
38
+ export function estimateTokensFromMessages(messages: TUniversalMessage[]): number {
39
+ return (
40
+ Math.ceil(messages.reduce((t, m) => t + (m.content?.length || 0), 0) / CHARS_PER_TOKEN) +
41
+ TOKEN_ESTIMATE_BUFFER
42
+ );
43
+ }
44
+
45
+ /** Check and update token-bucket state. Throws PluginError if any limit is exceeded. @internal */
46
+ export function checkTokenBucket(
47
+ bucket: ITokenBucket,
48
+ now: number,
49
+ estimatedTokens: number,
50
+ estimatedCost: number,
51
+ options: {
52
+ bucketSize: number;
53
+ refillRate: number;
54
+ timeWindow: number;
55
+ maxRequests: number;
56
+ maxCost: number;
57
+ },
58
+ pluginName: string,
59
+ ): void {
60
+ const timePassed = (now - bucket.lastRefill) / MS_PER_SECOND;
61
+ bucket.tokens = Math.min(options.bucketSize, bucket.tokens + timePassed * options.refillRate);
62
+ bucket.lastRefill = now;
63
+
64
+ if (bucket.tokens < estimatedTokens) {
65
+ throw new PluginError(
66
+ `Token bucket depleted. Available: ${Math.floor(bucket.tokens)}, Required: ${estimatedTokens}`,
67
+ pluginName,
68
+ { availableTokens: bucket.tokens, requiredTokens: estimatedTokens },
69
+ );
70
+ }
71
+
72
+ if (now - bucket.windowStart >= options.timeWindow) {
73
+ bucket.requests = 0;
74
+ bucket.cost = 0;
75
+ bucket.windowStart = now;
76
+ }
77
+
78
+ if (bucket.requests >= options.maxRequests) {
79
+ throw new PluginError(`Request limit exceeded. Max: ${options.maxRequests}`, pluginName, {
80
+ currentRequests: bucket.requests,
81
+ maxRequests: options.maxRequests,
82
+ });
83
+ }
84
+
85
+ if (bucket.cost + estimatedCost > options.maxCost) {
86
+ throw new PluginError(
87
+ `Cost limit exceeded. Current: $${bucket.cost.toFixed(COST_DECIMAL_PLACES)}, Estimated: $${estimatedCost.toFixed(COST_DECIMAL_PLACES)}, Max: $${options.maxCost}`,
88
+ pluginName,
89
+ { currentCost: bucket.cost, estimatedCost, maxCost: options.maxCost },
90
+ );
91
+ }
92
+
93
+ bucket.tokens -= estimatedTokens;
94
+ bucket.requests++;
95
+ }
96
+
97
+ /** Check and update sliding-window state. Throws PluginError if any limit is exceeded. @internal */
98
+ export function checkSlidingWindow(
99
+ window: ILimitWindow,
100
+ now: number,
101
+ estimatedTokens: number,
102
+ estimatedCost: number,
103
+ options: {
104
+ timeWindow: number;
105
+ maxTokens: number;
106
+ maxRequests: number;
107
+ maxCost: number;
108
+ },
109
+ pluginName: string,
110
+ ): void {
111
+ if (now - window.windowStart < options.timeWindow) {
112
+ if (window.tokens + estimatedTokens > options.maxTokens) {
113
+ throw new PluginError(
114
+ `Token limit exceeded in sliding window. Current: ${window.tokens}, Estimated: ${estimatedTokens}, Max: ${options.maxTokens}`,
115
+ pluginName,
116
+ { currentTokens: window.tokens, estimatedTokens, maxTokens: options.maxTokens },
117
+ );
118
+ }
119
+ if (window.count >= options.maxRequests) {
120
+ throw new PluginError(
121
+ `Request limit exceeded in sliding window. Current: ${window.count}, Max: ${options.maxRequests}`,
122
+ pluginName,
123
+ { currentRequests: window.count, maxRequests: options.maxRequests },
124
+ );
125
+ }
126
+ if (window.cost + estimatedCost > options.maxCost) {
127
+ throw new PluginError(
128
+ `Cost limit exceeded in sliding window. Current: $${window.cost.toFixed(COST_DECIMAL_PLACES)}, Estimated: $${estimatedCost.toFixed(COST_DECIMAL_PLACES)}, Max: $${options.maxCost}`,
129
+ pluginName,
130
+ { currentCost: window.cost, estimatedCost, maxCost: options.maxCost },
131
+ );
132
+ }
133
+ } else {
134
+ window.count = 0;
135
+ window.tokens = 0;
136
+ window.cost = 0;
137
+ window.windowStart = now;
138
+ }
139
+ window.count++;
140
+ }
141
+
142
+ /** Check and update fixed-window state. Throws PluginError if any limit is exceeded. @internal */
143
+ export function checkFixedWindow(
144
+ window: ILimitWindow,
145
+ now: number,
146
+ estimatedTokens: number,
147
+ estimatedCost: number,
148
+ options: {
149
+ timeWindow: number;
150
+ maxTokens: number;
151
+ maxRequests: number;
152
+ maxCost: number;
153
+ },
154
+ pluginName: string,
155
+ ): void {
156
+ if (now - window.windowStart >= options.timeWindow) {
157
+ window.count = 0;
158
+ window.tokens = 0;
159
+ window.cost = 0;
160
+ window.windowStart = now;
161
+ }
162
+
163
+ if (window.tokens + estimatedTokens > options.maxTokens) {
164
+ throw new PluginError(
165
+ `Token limit exceeded in fixed window. Current: ${window.tokens}, Estimated: ${estimatedTokens}, Max: ${options.maxTokens}`,
166
+ pluginName,
167
+ { currentTokens: window.tokens, estimatedTokens, maxTokens: options.maxTokens },
168
+ );
169
+ }
170
+ if (window.count >= options.maxRequests) {
171
+ throw new PluginError(
172
+ `Request limit exceeded in fixed window. Current: ${window.count}, Max: ${options.maxRequests}`,
173
+ pluginName,
174
+ { currentRequests: window.count, maxRequests: options.maxRequests },
175
+ );
176
+ }
177
+ if (window.cost + estimatedCost > options.maxCost) {
178
+ throw new PluginError(
179
+ `Cost limit exceeded in fixed window. Current: $${window.cost.toFixed(COST_DECIMAL_PLACES)}, Estimated: $${estimatedCost.toFixed(COST_DECIMAL_PLACES)}, Max: $${options.maxCost}`,
180
+ pluginName,
181
+ { currentCost: window.cost, estimatedCost, maxCost: options.maxCost },
182
+ );
183
+ }
184
+ window.count++;
185
+ }
@@ -0,0 +1,196 @@
1
+ import {
2
+ AbstractPlugin,
3
+ type IPluginExecutionContext,
4
+ type IPluginExecutionResult,
5
+ PluginCategory,
6
+ PluginPriority,
7
+ createLogger,
8
+ type ILogger,
9
+ type TUniversalMessage,
10
+ } from '@robota-sdk/agent-core';
11
+ import type {
12
+ TLimitsStrategy,
13
+ ILimitsPluginOptions,
14
+ TPluginLimitsStatusData,
15
+ ILimitWindow,
16
+ ITokenBucket,
17
+ } from './types';
18
+ import { validateLimitsOptions } from './validation';
19
+ import {
20
+ defaultCostCalculator,
21
+ estimateTokensFromMessages,
22
+ checkTokenBucket,
23
+ checkSlidingWindow,
24
+ checkFixedWindow,
25
+ } from './limits-helpers';
26
+
27
+ const DEFAULT_MAX_TOKENS = 100000;
28
+ const DEFAULT_MAX_REQUESTS = 1000;
29
+ const DEFAULT_TIME_WINDOW_MS = 3600000;
30
+ const DEFAULT_MAX_COST = 10.0;
31
+ const DEFAULT_TOKEN_COST_PER_1000 = 0.002;
32
+ const DEFAULT_REFILL_RATE = 100;
33
+ const DEFAULT_BUCKET_SIZE = 10000;
34
+
35
+ export type { TLimitsStrategy, ILimitsPluginOptions, TPluginLimitsStatusData };
36
+
37
+ export interface ILimitsPluginExecutionContext extends IPluginExecutionContext {
38
+ config?: { model?: string; maxTokens?: number; temperature?: number };
39
+ conversationId?: string;
40
+ }
41
+
42
+ export interface ILimitsPluginExecutionResult {
43
+ tokensUsed?: number;
44
+ cost?: number;
45
+ success?: boolean;
46
+ [key: string]: string | number | boolean | undefined;
47
+ }
48
+
49
+ /**
50
+ * Enforces rate limiting on token usage, request frequency, and cost.
51
+ * @extends AbstractPlugin
52
+ */
53
+ export class LimitsPlugin extends AbstractPlugin<ILimitsPluginOptions> {
54
+ name = 'LimitsPlugin';
55
+ version = '1.0.0';
56
+ private pluginOptions: Required<ILimitsPluginOptions>;
57
+ private logger: ILogger;
58
+ private windows = new Map<string, ILimitWindow>();
59
+ private buckets = new Map<string, ITokenBucket>();
60
+
61
+ constructor(options: ILimitsPluginOptions) {
62
+ super();
63
+ this.logger = createLogger('LimitsPlugin');
64
+ validateLimitsOptions(options, this.name, this.logger);
65
+ this.pluginOptions = {
66
+ enabled: options.enabled ?? true,
67
+ strategy: options.strategy,
68
+ maxTokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
69
+ maxRequests: options.maxRequests ?? DEFAULT_MAX_REQUESTS,
70
+ timeWindow: options.timeWindow ?? DEFAULT_TIME_WINDOW_MS,
71
+ maxCost: options.maxCost ?? DEFAULT_MAX_COST,
72
+ tokenCostPer1000: options.tokenCostPer1000 ?? DEFAULT_TOKEN_COST_PER_1000,
73
+ refillRate: options.refillRate ?? DEFAULT_REFILL_RATE,
74
+ bucketSize: options.bucketSize ?? DEFAULT_BUCKET_SIZE,
75
+ costCalculator:
76
+ options.costCalculator ??
77
+ ((tokens: number, model: string) =>
78
+ defaultCostCalculator(tokens, model, this.pluginOptions.tokenCostPer1000)),
79
+ category: options.category ?? PluginCategory.LIMITS,
80
+ priority: options.priority ?? PluginPriority.NORMAL,
81
+ moduleEvents: options.moduleEvents ?? [],
82
+ subscribeToAllModuleEvents: options.subscribeToAllModuleEvents ?? false,
83
+ };
84
+ }
85
+
86
+ override async beforeExecution(context: IPluginExecutionContext): Promise<void> {
87
+ if (this.pluginOptions.strategy === 'none') return;
88
+ const key = this.getKey(context);
89
+ const now = Date.now();
90
+ const est = estimateTokensFromMessages(context.messages ?? []);
91
+ const estCost = this.pluginOptions.costCalculator(est, this.resolveModelName(context));
92
+
93
+ switch (this.pluginOptions.strategy) {
94
+ case 'token-bucket':
95
+ checkTokenBucket(this.getBucket(key), now, est, estCost, this.pluginOptions, this.name);
96
+ break;
97
+ case 'sliding-window':
98
+ checkSlidingWindow(this.getWindow(key), now, est, estCost, this.pluginOptions, this.name);
99
+ break;
100
+ case 'fixed-window':
101
+ checkFixedWindow(this.getWindow(key), now, est, estCost, this.pluginOptions, this.name);
102
+ break;
103
+ }
104
+ }
105
+
106
+ override async afterExecution(
107
+ context: IPluginExecutionContext,
108
+ result: IPluginExecutionResult,
109
+ ): Promise<void> {
110
+ if (this.pluginOptions.strategy === 'none') return;
111
+ const key = this.getKey(context);
112
+ const tokensUsed = result?.tokensUsed || 0;
113
+ const cost = this.pluginOptions.costCalculator(tokensUsed, this.resolveModelName(context));
114
+ switch (this.pluginOptions.strategy) {
115
+ case 'token-bucket':
116
+ this.getBucket(key).cost += cost;
117
+ break;
118
+ case 'sliding-window':
119
+ case 'fixed-window': {
120
+ const w = this.getWindow(key);
121
+ w.tokens += tokensUsed;
122
+ w.cost += cost;
123
+ break;
124
+ }
125
+ }
126
+ }
127
+
128
+ private getBucket(key: string): ITokenBucket {
129
+ if (!this.buckets.has(key))
130
+ this.buckets.set(key, {
131
+ tokens: this.pluginOptions.bucketSize,
132
+ lastRefill: Date.now(),
133
+ requests: 0,
134
+ cost: 0,
135
+ windowStart: Date.now(),
136
+ });
137
+ return this.buckets.get(key)!;
138
+ }
139
+
140
+ private getWindow(key: string): ILimitWindow {
141
+ if (!this.windows.has(key))
142
+ this.windows.set(key, { count: 0, tokens: 0, cost: 0, windowStart: Date.now() });
143
+ return this.windows.get(key)!;
144
+ }
145
+
146
+ private getKey(context: ILimitsPluginExecutionContext): string {
147
+ return context.userId || context.sessionId || context.executionId || 'default';
148
+ }
149
+
150
+ private resolveModelName(context: IPluginExecutionContext): string {
151
+ const m = context.config?.model;
152
+ return typeof m === 'string' && m.length > 0 ? m : 'unknown';
153
+ }
154
+
155
+ getLimitsStatus(key?: string): TPluginLimitsStatusData {
156
+ if (key) {
157
+ const bucket = this.buckets.get(key);
158
+ const window = this.windows.get(key);
159
+ return {
160
+ strategy: this.pluginOptions.strategy,
161
+ key,
162
+ bucket: bucket
163
+ ? {
164
+ availableTokens: Math.floor(bucket.tokens),
165
+ requests: bucket.requests,
166
+ cost: bucket.cost,
167
+ }
168
+ : null,
169
+ window: window
170
+ ? {
171
+ count: window.count,
172
+ tokens: window.tokens,
173
+ cost: window.cost,
174
+ windowStart: window.windowStart,
175
+ }
176
+ : null,
177
+ };
178
+ }
179
+ return {
180
+ strategy: this.pluginOptions.strategy,
181
+ totalKeys: this.buckets.size + this.windows.size,
182
+ bucketKeys: Array.from(this.buckets.keys()),
183
+ windowKeys: Array.from(this.windows.keys()),
184
+ };
185
+ }
186
+
187
+ resetLimits(key?: string): void {
188
+ if (key) {
189
+ this.buckets.delete(key);
190
+ this.windows.delete(key);
191
+ } else {
192
+ this.buckets.clear();
193
+ this.windows.clear();
194
+ }
195
+ }
196
+ }
@@ -0,0 +1,73 @@
1
+ import type { IPluginOptions } from '@robota-sdk/agent-core';
2
+
3
+ /**
4
+ * Rate limiting strategies
5
+ */
6
+ export type TLimitsStrategy = 'token-bucket' | 'sliding-window' | 'fixed-window' | 'none';
7
+
8
+ /**
9
+ * Limits plugin configuration
10
+ */
11
+ export interface ILimitsPluginOptions extends IPluginOptions {
12
+ /** Rate limiting strategy */
13
+ strategy: TLimitsStrategy;
14
+ /** Maximum tokens per time window */
15
+ maxTokens?: number;
16
+ /** Maximum requests per time window */
17
+ maxRequests?: number;
18
+ /** Time window in milliseconds */
19
+ timeWindow?: number;
20
+ /** Maximum cost per time window (in USD) */
21
+ maxCost?: number;
22
+ /** Token cost per 1000 tokens (in USD) */
23
+ tokenCostPer1000?: number;
24
+ /** Bucket refill rate for token bucket strategy */
25
+ refillRate?: number;
26
+ /** Initial bucket size for token bucket strategy */
27
+ bucketSize?: number;
28
+ /** Custom cost calculator */
29
+ costCalculator?: (tokens: number, model: string) => number;
30
+ }
31
+
32
+ /**
33
+ * Plugin limits status data type - supports nested objects and null values for comprehensive status reporting
34
+ *
35
+ * REASON: Status data needs to include nested objects for bucket/window details and null values for missing data
36
+ * ALTERNATIVES_CONSIDERED:
37
+ * 1. Strict primitive types (loses nested status information)
38
+ * 2. Union types without null (breaks null handling)
39
+ * 3. Interface definitions (too rigid for dynamic status)
40
+ * 4. Generic constraints (too complex for status data)
41
+ * 5. Type assertions (decreases type safety)
42
+ * TODO: Consider specific status interfaces if patterns emerge
43
+ */
44
+ export type TPluginLimitsStatusData = Record<
45
+ string,
46
+ | string
47
+ | number
48
+ | boolean
49
+ | Array<string | number | boolean>
50
+ | Record<string, string | number | boolean>
51
+ | null
52
+ >;
53
+
54
+ /**
55
+ * Rate limiting window data
56
+ */
57
+ export interface ILimitWindow {
58
+ count: number;
59
+ tokens: number;
60
+ cost: number;
61
+ windowStart: number;
62
+ }
63
+
64
+ /**
65
+ * Token bucket state
66
+ */
67
+ export interface ITokenBucket {
68
+ tokens: number;
69
+ lastRefill: number;
70
+ requests: number;
71
+ cost: number;
72
+ windowStart: number;
73
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Validation logic for LimitsPlugin options.
3
+ *
4
+ * Extracted from limits-plugin.ts to keep each file under 300 lines.
5
+ * @internal
6
+ */
7
+ import { PluginError, type ILogger } from '@robota-sdk/agent-core';
8
+ import type { ILimitsPluginOptions } from './types';
9
+
10
+ /** Validate LimitsPlugin options. @internal */
11
+ export function validateLimitsOptions(
12
+ options: ILimitsPluginOptions,
13
+ pluginName: string,
14
+ logger: ILogger,
15
+ ): void {
16
+ if (options.strategy === 'none') {
17
+ logger.info('LimitsPlugin configured with "none" strategy - no rate limiting will be applied');
18
+ return;
19
+ }
20
+
21
+ if (!options.strategy) {
22
+ throw new PluginError(
23
+ 'Strategy must be specified for limits plugin. Use "none" to disable rate limiting, or choose from: token-bucket, sliding-window, fixed-window',
24
+ pluginName,
25
+ { availableStrategies: ['none', 'token-bucket', 'sliding-window', 'fixed-window'] },
26
+ );
27
+ }
28
+
29
+ const validStrategies = ['none', 'token-bucket', 'sliding-window', 'fixed-window'];
30
+ if (!validStrategies.includes(options.strategy)) {
31
+ throw new PluginError(
32
+ `Invalid strategy "${options.strategy}". Must be one of: ${validStrategies.join(', ')}`,
33
+ pluginName,
34
+ { provided: options.strategy, validStrategies },
35
+ );
36
+ }
37
+
38
+ if (options.strategy === 'token-bucket') {
39
+ if (options.bucketSize !== undefined && options.bucketSize <= 0)
40
+ throw new PluginError('Bucket size must be positive for token-bucket strategy', pluginName, {
41
+ strategy: options.strategy,
42
+ bucketSize: options.bucketSize,
43
+ });
44
+ if (options.refillRate !== undefined && options.refillRate < 0)
45
+ throw new PluginError(
46
+ 'Refill rate must be non-negative for token-bucket strategy',
47
+ pluginName,
48
+ { strategy: options.strategy, refillRate: options.refillRate },
49
+ );
50
+ }
51
+
52
+ if (['sliding-window', 'fixed-window'].includes(options.strategy)) {
53
+ if (options.timeWindow !== undefined && options.timeWindow <= 0)
54
+ throw new PluginError(
55
+ `Time window must be positive for ${options.strategy} strategy`,
56
+ pluginName,
57
+ { strategy: options.strategy, timeWindow: options.timeWindow },
58
+ );
59
+ }
60
+
61
+ if (options.maxRequests !== undefined && options.maxRequests < 0)
62
+ throw new PluginError('Max requests must be non-negative', pluginName, {
63
+ strategy: options.strategy,
64
+ maxRequests: options.maxRequests,
65
+ });
66
+ if (options.maxTokens !== undefined && options.maxTokens < 0)
67
+ throw new PluginError('Max tokens must be non-negative', pluginName, {
68
+ strategy: options.strategy,
69
+ maxTokens: options.maxTokens,
70
+ });
71
+ if (options.maxCost !== undefined && options.maxCost < 0)
72
+ throw new PluginError('Max cost must be non-negative', pluginName, {
73
+ strategy: options.strategy,
74
+ maxCost: options.maxCost,
75
+ });
76
+ if (options.tokenCostPer1000 !== undefined && options.tokenCostPer1000 < 0)
77
+ throw new PluginError('Token cost per 1000 must be non-negative', pluginName, {
78
+ strategy: options.strategy,
79
+ tokenCostPer1000: options.tokenCostPer1000,
80
+ });
81
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ConsoleLogFormatter, JsonLogFormatter } from '../formatters';
3
+ import type { ILogEntry } from '../types';
4
+
5
+ const entry: ILogEntry = {
6
+ timestamp: new Date('2025-01-01T12:00:00Z'),
7
+ level: 'info',
8
+ message: 'Test message',
9
+ context: { module: 'test' },
10
+ metadata: { operation: 'value' },
11
+ };
12
+
13
+ describe('ConsoleLogFormatter', () => {
14
+ const formatter = new ConsoleLogFormatter();
15
+
16
+ it('formats entry with all fields', () => {
17
+ const result = formatter.format(entry);
18
+ expect(result).toContain('2025-01-01T12:00:00.000Z');
19
+ expect(result).toContain('INFO');
20
+ expect(result).toContain('Test message');
21
+ expect(result).toContain('"module":"test"');
22
+ });
23
+
24
+ it('formats entry without context', () => {
25
+ const noCtx: ILogEntry = { ...entry, context: undefined };
26
+ const result = formatter.format(noCtx);
27
+ expect(result).toContain('Test message');
28
+ expect(result).not.toContain('module');
29
+ });
30
+
31
+ it('formats entry without metadata', () => {
32
+ const noMeta: ILogEntry = { ...entry, metadata: undefined };
33
+ const result = formatter.format(noMeta);
34
+ expect(result).not.toContain('"operation"');
35
+ });
36
+ });
37
+
38
+ describe('JsonLogFormatter', () => {
39
+ const formatter = new JsonLogFormatter();
40
+
41
+ it('formats entry as JSON string', () => {
42
+ const result = formatter.format(entry);
43
+ const parsed = JSON.parse(result);
44
+ expect(parsed.message).toBe('Test message');
45
+ expect(parsed.level).toBe('info');
46
+ expect(parsed.timestamp).toBe('2025-01-01T12:00:00.000Z');
47
+ });
48
+ });