@azumag/opencode-rate-limit-fallback 1.21.0 → 1.21.2
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.
- package/dist/index.d.ts +1 -2
- package/dist/index.js +0 -2
- package/dist/src/fallback/FallbackHandler.d.ts +73 -0
- package/dist/src/fallback/FallbackHandler.js +341 -0
- package/dist/src/fallback/ModelSelector.d.ts +37 -0
- package/dist/src/fallback/ModelSelector.js +134 -0
- package/dist/src/metrics/MetricsManager.d.ts +81 -0
- package/dist/src/metrics/MetricsManager.js +377 -0
- package/dist/src/metrics/types.d.ts +11 -0
- package/dist/src/metrics/types.js +11 -0
- package/dist/src/session/SubagentTracker.d.ts +36 -0
- package/dist/src/session/SubagentTracker.js +114 -0
- package/dist/src/types/index.d.ts +262 -0
- package/dist/src/types/index.js +46 -0
- package/dist/src/utils/config.d.ts +16 -0
- package/dist/src/utils/config.js +78 -0
- package/dist/src/utils/errorDetection.d.ts +7 -0
- package/dist/src/utils/errorDetection.js +34 -0
- package/dist/src/utils/helpers.d.ts +34 -0
- package/dist/src/utils/helpers.js +95 -0
- package/package.json +3 -5
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics Manager - Handles metrics collection, aggregation, and reporting
|
|
3
|
+
*/
|
|
4
|
+
import { RESET_INTERVAL_MS } from './types.js';
|
|
5
|
+
import { getModelKey } from '../utils/helpers.js';
|
|
6
|
+
/**
|
|
7
|
+
* Metrics Manager class for collecting and reporting metrics
|
|
8
|
+
*/
|
|
9
|
+
export class MetricsManager {
|
|
10
|
+
metrics;
|
|
11
|
+
config;
|
|
12
|
+
logger;
|
|
13
|
+
resetTimer = null;
|
|
14
|
+
constructor(config, logger) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.logger = logger;
|
|
17
|
+
this.metrics = {
|
|
18
|
+
rateLimits: new Map(),
|
|
19
|
+
fallbacks: {
|
|
20
|
+
total: 0,
|
|
21
|
+
successful: 0,
|
|
22
|
+
failed: 0,
|
|
23
|
+
averageDuration: 0,
|
|
24
|
+
byTargetModel: new Map(),
|
|
25
|
+
},
|
|
26
|
+
modelPerformance: new Map(),
|
|
27
|
+
startedAt: Date.now(),
|
|
28
|
+
generatedAt: Date.now(),
|
|
29
|
+
};
|
|
30
|
+
if (this.config.enabled) {
|
|
31
|
+
this.startResetTimer();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Start the automatic reset timer
|
|
36
|
+
*/
|
|
37
|
+
startResetTimer() {
|
|
38
|
+
if (this.resetTimer) {
|
|
39
|
+
clearInterval(this.resetTimer);
|
|
40
|
+
}
|
|
41
|
+
const intervalMs = RESET_INTERVAL_MS[this.config.resetInterval];
|
|
42
|
+
this.resetTimer = setInterval(() => {
|
|
43
|
+
this.reset();
|
|
44
|
+
}, intervalMs);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Reset all metrics data
|
|
48
|
+
*/
|
|
49
|
+
reset() {
|
|
50
|
+
this.metrics = {
|
|
51
|
+
rateLimits: new Map(),
|
|
52
|
+
fallbacks: {
|
|
53
|
+
total: 0,
|
|
54
|
+
successful: 0,
|
|
55
|
+
failed: 0,
|
|
56
|
+
averageDuration: 0,
|
|
57
|
+
byTargetModel: new Map(),
|
|
58
|
+
},
|
|
59
|
+
modelPerformance: new Map(),
|
|
60
|
+
startedAt: Date.now(),
|
|
61
|
+
generatedAt: Date.now(),
|
|
62
|
+
};
|
|
63
|
+
this.logger.debug("Metrics reset");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Record a rate limit event
|
|
67
|
+
*/
|
|
68
|
+
recordRateLimit(providerID, modelID) {
|
|
69
|
+
if (!this.config.enabled)
|
|
70
|
+
return;
|
|
71
|
+
const key = getModelKey(providerID, modelID);
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
const existing = this.metrics.rateLimits.get(key);
|
|
74
|
+
if (existing) {
|
|
75
|
+
const intervalMs = now - existing.lastOccurrence;
|
|
76
|
+
existing.count++;
|
|
77
|
+
existing.lastOccurrence = now;
|
|
78
|
+
existing.averageInterval = existing.averageInterval
|
|
79
|
+
? (existing.averageInterval + intervalMs) / 2
|
|
80
|
+
: intervalMs;
|
|
81
|
+
this.metrics.rateLimits.set(key, existing);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
this.metrics.rateLimits.set(key, {
|
|
85
|
+
count: 1,
|
|
86
|
+
firstOccurrence: now,
|
|
87
|
+
lastOccurrence: now,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Record the start of a fallback operation
|
|
93
|
+
* @returns timestamp for tracking duration
|
|
94
|
+
*/
|
|
95
|
+
recordFallbackStart() {
|
|
96
|
+
if (!this.config.enabled)
|
|
97
|
+
return 0;
|
|
98
|
+
return Date.now();
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Record a successful fallback operation
|
|
102
|
+
*/
|
|
103
|
+
recordFallbackSuccess(targetProviderID, targetModelID, startTime) {
|
|
104
|
+
if (!this.config.enabled)
|
|
105
|
+
return;
|
|
106
|
+
const duration = Date.now() - startTime;
|
|
107
|
+
const key = getModelKey(targetProviderID, targetModelID);
|
|
108
|
+
this.metrics.fallbacks.total++;
|
|
109
|
+
this.metrics.fallbacks.successful++;
|
|
110
|
+
// Update average duration
|
|
111
|
+
const totalDuration = this.metrics.fallbacks.averageDuration * (this.metrics.fallbacks.successful - 1);
|
|
112
|
+
this.metrics.fallbacks.averageDuration = (totalDuration + duration) / this.metrics.fallbacks.successful;
|
|
113
|
+
// Update target model metrics
|
|
114
|
+
const targetMetrics = this.metrics.fallbacks.byTargetModel.get(key) || {
|
|
115
|
+
usedAsFallback: 0,
|
|
116
|
+
successful: 0,
|
|
117
|
+
failed: 0,
|
|
118
|
+
};
|
|
119
|
+
targetMetrics.usedAsFallback++;
|
|
120
|
+
targetMetrics.successful++;
|
|
121
|
+
this.metrics.fallbacks.byTargetModel.set(key, targetMetrics);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Record a failed fallback operation
|
|
125
|
+
*/
|
|
126
|
+
recordFallbackFailure() {
|
|
127
|
+
if (!this.config.enabled)
|
|
128
|
+
return;
|
|
129
|
+
this.metrics.fallbacks.total++;
|
|
130
|
+
this.metrics.fallbacks.failed++;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Record a model request
|
|
134
|
+
*/
|
|
135
|
+
recordModelRequest(providerID, modelID) {
|
|
136
|
+
if (!this.config.enabled)
|
|
137
|
+
return;
|
|
138
|
+
const key = getModelKey(providerID, modelID);
|
|
139
|
+
const existing = this.metrics.modelPerformance.get(key) || {
|
|
140
|
+
requests: 0,
|
|
141
|
+
successes: 0,
|
|
142
|
+
failures: 0,
|
|
143
|
+
};
|
|
144
|
+
existing.requests++;
|
|
145
|
+
this.metrics.modelPerformance.set(key, existing);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Record a successful model request
|
|
149
|
+
*/
|
|
150
|
+
recordModelSuccess(providerID, modelID, responseTime) {
|
|
151
|
+
if (!this.config.enabled)
|
|
152
|
+
return;
|
|
153
|
+
const key = getModelKey(providerID, modelID);
|
|
154
|
+
const existing = this.metrics.modelPerformance.get(key) || {
|
|
155
|
+
requests: 0,
|
|
156
|
+
successes: 0,
|
|
157
|
+
failures: 0,
|
|
158
|
+
};
|
|
159
|
+
existing.successes++;
|
|
160
|
+
// Update average response time
|
|
161
|
+
const totalTime = (existing.averageResponseTime || 0) * (existing.successes - 1);
|
|
162
|
+
existing.averageResponseTime = (totalTime + responseTime) / existing.successes;
|
|
163
|
+
this.metrics.modelPerformance.set(key, existing);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Record a failed model request
|
|
167
|
+
*/
|
|
168
|
+
recordModelFailure(providerID, modelID) {
|
|
169
|
+
if (!this.config.enabled)
|
|
170
|
+
return;
|
|
171
|
+
const key = getModelKey(providerID, modelID);
|
|
172
|
+
const existing = this.metrics.modelPerformance.get(key) || {
|
|
173
|
+
requests: 0,
|
|
174
|
+
successes: 0,
|
|
175
|
+
failures: 0,
|
|
176
|
+
};
|
|
177
|
+
existing.failures++;
|
|
178
|
+
this.metrics.modelPerformance.set(key, existing);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get a copy of the current metrics
|
|
182
|
+
*/
|
|
183
|
+
getMetrics() {
|
|
184
|
+
this.metrics.generatedAt = Date.now();
|
|
185
|
+
return { ...this.metrics };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Export metrics in the specified format
|
|
189
|
+
*/
|
|
190
|
+
export(format = "json") {
|
|
191
|
+
const metrics = this.getMetrics();
|
|
192
|
+
switch (format) {
|
|
193
|
+
case "pretty":
|
|
194
|
+
return this.exportPretty(metrics);
|
|
195
|
+
case "csv":
|
|
196
|
+
return this.exportCSV(metrics);
|
|
197
|
+
case "json":
|
|
198
|
+
default:
|
|
199
|
+
return JSON.stringify(this.toPlainObject(metrics), null, 2);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Convert metrics to a plain object (converts Maps to Objects)
|
|
204
|
+
*/
|
|
205
|
+
toPlainObject(metrics) {
|
|
206
|
+
return {
|
|
207
|
+
rateLimits: Object.fromEntries(Array.from(metrics.rateLimits.entries()).map(([k, v]) => [k, v])),
|
|
208
|
+
fallbacks: {
|
|
209
|
+
...metrics.fallbacks,
|
|
210
|
+
byTargetModel: Object.fromEntries(Array.from(metrics.fallbacks.byTargetModel.entries()).map(([k, v]) => [k, v])),
|
|
211
|
+
},
|
|
212
|
+
modelPerformance: Object.fromEntries(Array.from(metrics.modelPerformance.entries()).map(([k, v]) => [k, v])),
|
|
213
|
+
startedAt: metrics.startedAt,
|
|
214
|
+
generatedAt: metrics.generatedAt,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Export metrics in pretty-printed text format
|
|
219
|
+
*/
|
|
220
|
+
exportPretty(metrics) {
|
|
221
|
+
const lines = [];
|
|
222
|
+
lines.push("=".repeat(60));
|
|
223
|
+
lines.push("Rate Limit Fallback Metrics");
|
|
224
|
+
lines.push("=".repeat(60));
|
|
225
|
+
lines.push(`Started: ${new Date(metrics.startedAt).toISOString()}`);
|
|
226
|
+
lines.push(`Generated: ${new Date(metrics.generatedAt).toISOString()}`);
|
|
227
|
+
lines.push("");
|
|
228
|
+
// Rate Limits
|
|
229
|
+
lines.push("Rate Limits:");
|
|
230
|
+
lines.push("-".repeat(40));
|
|
231
|
+
if (metrics.rateLimits.size === 0) {
|
|
232
|
+
lines.push(" No rate limits recorded");
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
for (const [model, data] of metrics.rateLimits.entries()) {
|
|
236
|
+
lines.push(` ${model}:`);
|
|
237
|
+
lines.push(` Count: ${data.count}`);
|
|
238
|
+
lines.push(` First: ${new Date(data.firstOccurrence).toISOString()}`);
|
|
239
|
+
lines.push(` Last: ${new Date(data.lastOccurrence).toISOString()}`);
|
|
240
|
+
if (data.averageInterval) {
|
|
241
|
+
lines.push(` Avg Interval: ${(data.averageInterval / 1000).toFixed(2)}s`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
lines.push("");
|
|
246
|
+
// Fallbacks
|
|
247
|
+
lines.push("Fallbacks:");
|
|
248
|
+
lines.push("-".repeat(40));
|
|
249
|
+
lines.push(` Total: ${metrics.fallbacks.total}`);
|
|
250
|
+
lines.push(` Successful: ${metrics.fallbacks.successful}`);
|
|
251
|
+
lines.push(` Failed: ${metrics.fallbacks.failed}`);
|
|
252
|
+
if (metrics.fallbacks.averageDuration > 0) {
|
|
253
|
+
lines.push(` Avg Duration: ${(metrics.fallbacks.averageDuration / 1000).toFixed(2)}s`);
|
|
254
|
+
}
|
|
255
|
+
if (metrics.fallbacks.byTargetModel.size > 0) {
|
|
256
|
+
lines.push("");
|
|
257
|
+
lines.push(" By Target Model:");
|
|
258
|
+
for (const [model, data] of metrics.fallbacks.byTargetModel.entries()) {
|
|
259
|
+
lines.push(` ${model}:`);
|
|
260
|
+
lines.push(` Used: ${data.usedAsFallback}`);
|
|
261
|
+
lines.push(` Success: ${data.successful}`);
|
|
262
|
+
lines.push(` Failed: ${data.failed}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
lines.push("");
|
|
266
|
+
// Model Performance
|
|
267
|
+
lines.push("Model Performance:");
|
|
268
|
+
lines.push("-".repeat(40));
|
|
269
|
+
if (metrics.modelPerformance.size === 0) {
|
|
270
|
+
lines.push(" No performance data recorded");
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
for (const [model, data] of metrics.modelPerformance.entries()) {
|
|
274
|
+
lines.push(` ${model}:`);
|
|
275
|
+
lines.push(` Requests: ${data.requests}`);
|
|
276
|
+
lines.push(` Successes: ${data.successes}`);
|
|
277
|
+
lines.push(` Failures: ${data.failures}`);
|
|
278
|
+
if (data.averageResponseTime) {
|
|
279
|
+
lines.push(` Avg Response: ${(data.averageResponseTime / 1000).toFixed(2)}s`);
|
|
280
|
+
}
|
|
281
|
+
if (data.requests > 0) {
|
|
282
|
+
const successRate = ((data.successes / data.requests) * 100).toFixed(1);
|
|
283
|
+
lines.push(` Success Rate: ${successRate}%`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return lines.join("\n");
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Export metrics in CSV format
|
|
291
|
+
*/
|
|
292
|
+
exportCSV(metrics) {
|
|
293
|
+
const lines = [];
|
|
294
|
+
// Rate Limits CSV
|
|
295
|
+
lines.push("=== RATE_LIMITS ===");
|
|
296
|
+
lines.push("model,count,first_occurrence,last_occurrence,avg_interval_ms");
|
|
297
|
+
for (const [model, data] of metrics.rateLimits.entries()) {
|
|
298
|
+
lines.push([
|
|
299
|
+
model,
|
|
300
|
+
data.count,
|
|
301
|
+
data.firstOccurrence,
|
|
302
|
+
data.lastOccurrence,
|
|
303
|
+
data.averageInterval || 0,
|
|
304
|
+
].join(","));
|
|
305
|
+
}
|
|
306
|
+
lines.push("");
|
|
307
|
+
// Fallbacks Summary CSV
|
|
308
|
+
lines.push("=== FALLBACKS_SUMMARY ===");
|
|
309
|
+
lines.push(`total,successful,failed,avg_duration_ms`);
|
|
310
|
+
lines.push([
|
|
311
|
+
metrics.fallbacks.total,
|
|
312
|
+
metrics.fallbacks.successful,
|
|
313
|
+
metrics.fallbacks.failed,
|
|
314
|
+
metrics.fallbacks.averageDuration || 0,
|
|
315
|
+
].join(","));
|
|
316
|
+
lines.push("");
|
|
317
|
+
// Fallbacks by Model CSV
|
|
318
|
+
lines.push("=== FALLBACKS_BY_MODEL ===");
|
|
319
|
+
lines.push("model,used_as_fallback,successful,failed");
|
|
320
|
+
for (const [model, data] of metrics.fallbacks.byTargetModel.entries()) {
|
|
321
|
+
lines.push([
|
|
322
|
+
model,
|
|
323
|
+
data.usedAsFallback,
|
|
324
|
+
data.successful,
|
|
325
|
+
data.failed,
|
|
326
|
+
].join(","));
|
|
327
|
+
}
|
|
328
|
+
lines.push("");
|
|
329
|
+
// Model Performance CSV
|
|
330
|
+
lines.push("=== MODEL_PERFORMANCE ===");
|
|
331
|
+
lines.push("model,requests,successes,failures,avg_response_time_ms,success_rate");
|
|
332
|
+
for (const [model, data] of metrics.modelPerformance.entries()) {
|
|
333
|
+
const successRate = data.requests > 0 ? ((data.successes / data.requests) * 100).toFixed(1) : "0";
|
|
334
|
+
lines.push([
|
|
335
|
+
model,
|
|
336
|
+
data.requests,
|
|
337
|
+
data.successes,
|
|
338
|
+
data.failures,
|
|
339
|
+
data.averageResponseTime || 0,
|
|
340
|
+
successRate,
|
|
341
|
+
].join(","));
|
|
342
|
+
}
|
|
343
|
+
return lines.join("\n");
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Report metrics to configured outputs
|
|
347
|
+
*/
|
|
348
|
+
async report() {
|
|
349
|
+
if (!this.config.enabled)
|
|
350
|
+
return;
|
|
351
|
+
const output = this.export(this.config.output.format);
|
|
352
|
+
// Console output
|
|
353
|
+
if (this.config.output.console) {
|
|
354
|
+
console.log(output);
|
|
355
|
+
}
|
|
356
|
+
// File output
|
|
357
|
+
if (this.config.output.file) {
|
|
358
|
+
try {
|
|
359
|
+
const { writeFileSync } = await import('fs');
|
|
360
|
+
writeFileSync(this.config.output.file, output, "utf-8");
|
|
361
|
+
this.logger.debug(`Metrics exported to ${this.config.output.file}`);
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
this.logger.warn(`Failed to write metrics to file: ${this.config.output.file}`, { error });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Clean up resources
|
|
370
|
+
*/
|
|
371
|
+
destroy() {
|
|
372
|
+
if (this.resetTimer) {
|
|
373
|
+
clearInterval(this.resetTimer);
|
|
374
|
+
this.resetTimer = null;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics-specific types for the MetricsManager
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Reset interval options for metrics
|
|
6
|
+
*/
|
|
7
|
+
export type ResetInterval = "hourly" | "daily" | "weekly";
|
|
8
|
+
/**
|
|
9
|
+
* Reset interval values in milliseconds
|
|
10
|
+
*/
|
|
11
|
+
export declare const RESET_INTERVAL_MS: Record<ResetInterval, number>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent hierarchy and fallback propagation management
|
|
3
|
+
*/
|
|
4
|
+
import type { SessionHierarchy, PluginConfig } from '../types/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Initialize subagent tracker with config
|
|
7
|
+
*/
|
|
8
|
+
export declare function initSubagentTracker(config: PluginConfig): void;
|
|
9
|
+
/**
|
|
10
|
+
* Register a new subagent in the hierarchy
|
|
11
|
+
*/
|
|
12
|
+
export declare function registerSubagent(sessionID: string, parentSessionID: string, config: PluginConfig): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Get root session ID for a session
|
|
15
|
+
*/
|
|
16
|
+
export declare function getRootSession(sessionID: string): string | null;
|
|
17
|
+
/**
|
|
18
|
+
* Get hierarchy for a session
|
|
19
|
+
*/
|
|
20
|
+
export declare function getHierarchy(sessionID: string): SessionHierarchy | null;
|
|
21
|
+
/**
|
|
22
|
+
* Get all session hierarchies (for cleanup)
|
|
23
|
+
*/
|
|
24
|
+
export declare function getAllHierarchies(): Map<string, SessionHierarchy>;
|
|
25
|
+
/**
|
|
26
|
+
* Get session to root map (for cleanup)
|
|
27
|
+
*/
|
|
28
|
+
export declare function getSessionToRootMap(): Map<string, string>;
|
|
29
|
+
/**
|
|
30
|
+
* Clean up stale hierarchies
|
|
31
|
+
*/
|
|
32
|
+
export declare function cleanupStaleEntries(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Clean up all hierarchies
|
|
35
|
+
*/
|
|
36
|
+
export declare function clearAll(): void;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent hierarchy and fallback propagation management
|
|
3
|
+
*/
|
|
4
|
+
import { SESSION_ENTRY_TTL_MS } from '../types/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Session Hierarchy storage
|
|
7
|
+
*/
|
|
8
|
+
const sessionHierarchies = new Map();
|
|
9
|
+
const sessionToRootMap = new Map();
|
|
10
|
+
let maxSubagentDepth = 10;
|
|
11
|
+
/**
|
|
12
|
+
* Initialize subagent tracker with config
|
|
13
|
+
*/
|
|
14
|
+
export function initSubagentTracker(config) {
|
|
15
|
+
maxSubagentDepth = config.maxSubagentDepth ?? 10;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Register a new subagent in the hierarchy
|
|
19
|
+
*/
|
|
20
|
+
export function registerSubagent(sessionID, parentSessionID, config) {
|
|
21
|
+
// Validate parent session exists
|
|
22
|
+
// Parent session must either be registered in sessionToRootMap or be a new root session
|
|
23
|
+
const parentRootSessionID = sessionToRootMap.get(parentSessionID);
|
|
24
|
+
// Determine root session - if parent doesn't exist, treat it as a new root
|
|
25
|
+
const rootSessionID = parentRootSessionID || parentSessionID;
|
|
26
|
+
// If parent is not a subagent but we're treating it as a root, create a hierarchy for it
|
|
27
|
+
// This allows sessions to become roots when their first subagent is registered
|
|
28
|
+
const hierarchy = getOrCreateHierarchy(rootSessionID, config);
|
|
29
|
+
const parentSubagent = hierarchy.subagents.get(parentSessionID);
|
|
30
|
+
const depth = parentSubagent ? parentSubagent.depth + 1 : 1;
|
|
31
|
+
// Enforce max depth
|
|
32
|
+
if (depth > maxSubagentDepth) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const subagent = {
|
|
36
|
+
sessionID,
|
|
37
|
+
parentSessionID,
|
|
38
|
+
depth,
|
|
39
|
+
fallbackState: "none",
|
|
40
|
+
createdAt: Date.now(),
|
|
41
|
+
lastActivity: Date.now(),
|
|
42
|
+
};
|
|
43
|
+
hierarchy.subagents.set(sessionID, subagent);
|
|
44
|
+
sessionToRootMap.set(sessionID, rootSessionID);
|
|
45
|
+
hierarchy.lastActivity = Date.now();
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get or create hierarchy for a root session
|
|
50
|
+
*/
|
|
51
|
+
function getOrCreateHierarchy(rootSessionID, config) {
|
|
52
|
+
let hierarchy = sessionHierarchies.get(rootSessionID);
|
|
53
|
+
if (!hierarchy) {
|
|
54
|
+
hierarchy = {
|
|
55
|
+
rootSessionID,
|
|
56
|
+
subagents: new Map(),
|
|
57
|
+
sharedFallbackState: "none",
|
|
58
|
+
sharedConfig: config,
|
|
59
|
+
createdAt: Date.now(),
|
|
60
|
+
lastActivity: Date.now(),
|
|
61
|
+
};
|
|
62
|
+
sessionHierarchies.set(rootSessionID, hierarchy);
|
|
63
|
+
sessionToRootMap.set(rootSessionID, rootSessionID);
|
|
64
|
+
}
|
|
65
|
+
return hierarchy;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get root session ID for a session
|
|
69
|
+
*/
|
|
70
|
+
export function getRootSession(sessionID) {
|
|
71
|
+
return sessionToRootMap.get(sessionID) || null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get hierarchy for a session
|
|
75
|
+
*/
|
|
76
|
+
export function getHierarchy(sessionID) {
|
|
77
|
+
const rootSessionID = getRootSession(sessionID);
|
|
78
|
+
return rootSessionID ? sessionHierarchies.get(rootSessionID) || null : null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get all session hierarchies (for cleanup)
|
|
82
|
+
*/
|
|
83
|
+
export function getAllHierarchies() {
|
|
84
|
+
return sessionHierarchies;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get session to root map (for cleanup)
|
|
88
|
+
*/
|
|
89
|
+
export function getSessionToRootMap() {
|
|
90
|
+
return sessionToRootMap;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Clean up stale hierarchies
|
|
94
|
+
*/
|
|
95
|
+
export function cleanupStaleEntries() {
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
for (const [rootSessionID, hierarchy] of sessionHierarchies.entries()) {
|
|
98
|
+
if (now - hierarchy.lastActivity > SESSION_ENTRY_TTL_MS) {
|
|
99
|
+
// Clean up all subagents in this hierarchy
|
|
100
|
+
for (const subagentID of hierarchy.subagents.keys()) {
|
|
101
|
+
sessionToRootMap.delete(subagentID);
|
|
102
|
+
}
|
|
103
|
+
sessionHierarchies.delete(rootSessionID);
|
|
104
|
+
sessionToRootMap.delete(rootSessionID);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Clean up all hierarchies
|
|
110
|
+
*/
|
|
111
|
+
export function clearAll() {
|
|
112
|
+
sessionHierarchies.clear();
|
|
113
|
+
sessionToRootMap.clear();
|
|
114
|
+
}
|