@stackmemoryai/stackmemory 1.0.1 → 1.2.0

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 (31) hide show
  1. package/dist/src/cli/commands/audit.js +134 -0
  2. package/dist/src/cli/commands/bench.js +252 -0
  3. package/dist/src/cli/commands/dashboard.js +2 -1
  4. package/dist/src/cli/commands/stats.js +118 -0
  5. package/dist/src/cli/index.js +6 -0
  6. package/dist/src/core/config/feature-flags.js +7 -1
  7. package/dist/src/core/context/enhanced-rehydration.js +24 -5
  8. package/dist/src/core/extensions/cerebras-adapter.js +28 -0
  9. package/dist/src/core/extensions/deepinfra-adapter.js +32 -0
  10. package/dist/src/core/extensions/provider-adapter.js +33 -240
  11. package/dist/src/core/models/complexity-scorer.js +154 -0
  12. package/dist/src/core/models/model-router.js +230 -36
  13. package/dist/src/core/models/provider-pricing.js +63 -0
  14. package/dist/src/core/models/sensitive-guard.js +112 -0
  15. package/dist/src/core/monitoring/feedback-loops.js +88 -0
  16. package/dist/src/hooks/schemas.js +12 -1
  17. package/dist/src/integrations/anthropic/batch-client.js +256 -0
  18. package/dist/src/integrations/anthropic/client.js +87 -72
  19. package/dist/src/integrations/claude-code/subagent-client.js +133 -12
  20. package/dist/src/integrations/graphiti/client.js +16 -4
  21. package/dist/src/integrations/mcp/handlers/index.js +25 -1
  22. package/dist/src/integrations/mcp/handlers/provider-handlers.js +227 -0
  23. package/dist/src/integrations/mcp/server.js +207 -1
  24. package/dist/src/integrations/mcp/tool-definitions.js +38 -1
  25. package/dist/src/orchestrators/multimodal/baselines.js +128 -0
  26. package/dist/src/orchestrators/multimodal/constants.js +9 -1
  27. package/dist/src/orchestrators/multimodal/harness.js +86 -6
  28. package/dist/src/orchestrators/multimodal/providers.js +113 -2
  29. package/dist/src/skills/recursive-agent-orchestrator.js +15 -10
  30. package/dist/src/utils/fuzzy-edit.js +162 -0
  31. package/package.json +5 -1
@@ -10,6 +10,40 @@ import {
10
10
  ModelRouterConfigSchema,
11
11
  parseConfigSafe
12
12
  } from "../../hooks/schemas.js";
13
+ import { isFeatureEnabled } from "../config/feature-flags.js";
14
+ import { scoreComplexity } from "./complexity-scorer.js";
15
+ import {
16
+ detectSensitiveContent,
17
+ isApprovedProvider
18
+ } from "./sensitive-guard.js";
19
+ const MODEL_TOKEN_LIMITS = {
20
+ // Claude 4.x / 4.5 / 4.6
21
+ "claude-opus-4-6": 2e5,
22
+ "claude-sonnet-4-5-20250929": 2e5,
23
+ "claude-haiku-4-5-20251001": 2e5,
24
+ "claude-sonnet-4-20250514": 2e5,
25
+ // Claude 3.x
26
+ "claude-3-5-sonnet-20241022": 2e5,
27
+ "claude-3-5-haiku-20241022": 2e5,
28
+ "claude-3-opus-20240229": 2e5,
29
+ // OpenAI
30
+ "gpt-4o": 128e3,
31
+ "gpt-4-turbo": 128e3,
32
+ "gpt-4": 8192,
33
+ o1: 2e5,
34
+ "o3-mini": 2e5,
35
+ // Qwen
36
+ "qwen3-max-2025-01-23": 128e3,
37
+ // Cerebras
38
+ "llama-4-scout-17b-16e-instruct": 131072,
39
+ // DeepInfra
40
+ "THUDM/glm-4-9b-chat": 128e3
41
+ };
42
+ const DEFAULT_MODEL_TOKEN_LIMIT = 2e5;
43
+ function getModelTokenLimit(model) {
44
+ if (!model) return DEFAULT_MODEL_TOKEN_LIMIT;
45
+ return MODEL_TOKEN_LIMITS[model] ?? DEFAULT_MODEL_TOKEN_LIMIT;
46
+ }
13
47
  const CONFIG_PATH = join(homedir(), ".stackmemory", "model-router.json");
14
48
  const DEFAULT_CONFIG = {
15
49
  enabled: false,
@@ -40,6 +74,29 @@ const DEFAULT_CONFIG = {
40
74
  enable_thinking: true,
41
75
  thinking_budget: 1e4
42
76
  }
77
+ },
78
+ cerebras: {
79
+ provider: "cerebras",
80
+ model: "llama-4-scout-17b-16e-instruct",
81
+ baseUrl: "https://api.cerebras.ai/v1",
82
+ apiKeyEnv: "CEREBRAS_API_KEY"
83
+ },
84
+ deepinfra: {
85
+ provider: "deepinfra",
86
+ model: "THUDM/glm-4-9b-chat",
87
+ baseUrl: "https://api.deepinfra.com/v1/openai",
88
+ apiKeyEnv: "DEEPINFRA_API_KEY"
89
+ },
90
+ openrouter: {
91
+ provider: "openrouter",
92
+ model: "meta-llama/llama-4-scout",
93
+ baseUrl: "https://openrouter.ai/api",
94
+ apiKeyEnv: "OPENROUTER_API_KEY"
95
+ },
96
+ "anthropic-batch": {
97
+ provider: "anthropic-batch",
98
+ model: "claude-sonnet-4-5-20250929",
99
+ apiKeyEnv: "ANTHROPIC_API_KEY"
43
100
  }
44
101
  },
45
102
  thinkingMode: {
@@ -49,28 +106,43 @@ const DEFAULT_CONFIG = {
49
106
  topP: 0.95
50
107
  }
51
108
  };
109
+ let _configCache = null;
110
+ const CONFIG_CACHE_TTL_MS = 5e3;
52
111
  function loadModelRouterConfig() {
112
+ const now = Date.now();
113
+ if (_configCache && now < _configCache.expiresAt) {
114
+ return _configCache.config;
115
+ }
116
+ let config;
53
117
  try {
54
118
  if (existsSync(CONFIG_PATH)) {
55
119
  const data = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
56
- return parseConfigSafe(
120
+ config = parseConfigSafe(
57
121
  ModelRouterConfigSchema,
58
122
  { ...DEFAULT_CONFIG, ...data },
59
123
  DEFAULT_CONFIG,
60
124
  "model-router"
61
125
  );
126
+ } else {
127
+ config = { ...DEFAULT_CONFIG };
62
128
  }
63
129
  } catch {
130
+ config = { ...DEFAULT_CONFIG };
64
131
  }
65
- return { ...DEFAULT_CONFIG };
132
+ _configCache = { config, expiresAt: now + CONFIG_CACHE_TTL_MS };
133
+ return config;
66
134
  }
67
135
  function saveModelRouterConfig(config) {
68
136
  try {
69
137
  ensureSecureDir(join(homedir(), ".stackmemory"));
70
138
  writeFileSecure(CONFIG_PATH, JSON.stringify(config, null, 2));
139
+ _configCache = null;
71
140
  } catch {
72
141
  }
73
142
  }
143
+ function invalidateConfigCache() {
144
+ _configCache = null;
145
+ }
74
146
  function getModelForTask(taskType) {
75
147
  const config = loadModelRouterConfig();
76
148
  if (!config.enabled) {
@@ -124,6 +196,156 @@ function requiresDeepThinking(input) {
124
196
  ];
125
197
  return thinkPatterns.some((pattern) => pattern.test(input));
126
198
  }
199
+ const OPTIMAL_ROUTING = {
200
+ linting: {
201
+ provider: "deepinfra",
202
+ model: "THUDM/glm-4-9b-chat",
203
+ apiKeyEnv: "DEEPINFRA_API_KEY",
204
+ baseUrl: "https://api.deepinfra.com/v1/openai"
205
+ },
206
+ context: {
207
+ provider: "deepinfra",
208
+ model: "THUDM/glm-4-9b-chat",
209
+ apiKeyEnv: "DEEPINFRA_API_KEY",
210
+ baseUrl: "https://api.deepinfra.com/v1/openai"
211
+ },
212
+ code: {
213
+ provider: "cerebras",
214
+ model: "llama-4-scout-17b-16e-instruct",
215
+ apiKeyEnv: "CEREBRAS_API_KEY",
216
+ baseUrl: "https://api.cerebras.ai/v1"
217
+ },
218
+ testing: {
219
+ provider: "cerebras",
220
+ model: "llama-4-scout-17b-16e-instruct",
221
+ apiKeyEnv: "CEREBRAS_API_KEY",
222
+ baseUrl: "https://api.cerebras.ai/v1"
223
+ },
224
+ review: {
225
+ provider: "anthropic",
226
+ model: "claude-sonnet-4-5-20250929",
227
+ apiKeyEnv: "ANTHROPIC_API_KEY"
228
+ },
229
+ plan: {
230
+ provider: "anthropic",
231
+ model: "claude-sonnet-4-5-20250929",
232
+ apiKeyEnv: "ANTHROPIC_API_KEY"
233
+ },
234
+ think: {
235
+ provider: "anthropic",
236
+ model: "claude-sonnet-4-5-20250929",
237
+ apiKeyEnv: "ANTHROPIC_API_KEY"
238
+ }
239
+ };
240
+ const FALLBACK_CHAIN = ["deepinfra", "cerebras", "anthropic"];
241
+ const CHEAP_PROVIDERS = [
242
+ {
243
+ provider: "openrouter",
244
+ model: "meta-llama/llama-4-scout",
245
+ apiKeyEnv: "OPENROUTER_API_KEY",
246
+ baseUrl: "https://openrouter.ai/api"
247
+ },
248
+ {
249
+ provider: "deepinfra",
250
+ model: "THUDM/glm-4-9b-chat",
251
+ apiKeyEnv: "DEEPINFRA_API_KEY",
252
+ baseUrl: "https://api.deepinfra.com/v1/openai"
253
+ },
254
+ {
255
+ provider: "cerebras",
256
+ model: "llama-4-scout-17b-16e-instruct",
257
+ apiKeyEnv: "CEREBRAS_API_KEY",
258
+ baseUrl: "https://api.cerebras.ai/v1"
259
+ }
260
+ ];
261
+ function getOptimalProvider(taskType, preference, complexityInput) {
262
+ const defaultResult = {
263
+ provider: "anthropic",
264
+ model: "claude-sonnet-4-5-20250929",
265
+ apiKeyEnv: "ANTHROPIC_API_KEY"
266
+ };
267
+ if (!isFeatureEnabled("multiProvider")) {
268
+ return defaultResult;
269
+ }
270
+ if (complexityInput) {
271
+ const sensitiveCheck = detectSensitiveContent(
272
+ complexityInput.task,
273
+ complexityInput.context
274
+ );
275
+ if (sensitiveCheck.sensitive) {
276
+ return defaultResult;
277
+ }
278
+ }
279
+ if (preference) {
280
+ if (!isApprovedProvider(preference) && complexityInput) {
281
+ const check = detectSensitiveContent(
282
+ complexityInput.task,
283
+ complexityInput.context
284
+ );
285
+ if (check.sensitive) {
286
+ return defaultResult;
287
+ }
288
+ }
289
+ const config = loadModelRouterConfig();
290
+ const providerConfig = config.providers[preference];
291
+ if (providerConfig && process.env[providerConfig.apiKeyEnv]) {
292
+ return {
293
+ provider: preference,
294
+ model: providerConfig.model,
295
+ baseUrl: providerConfig.baseUrl,
296
+ apiKeyEnv: providerConfig.apiKeyEnv
297
+ };
298
+ }
299
+ }
300
+ if (complexityInput) {
301
+ const complexity = scoreComplexity(
302
+ complexityInput.task,
303
+ complexityInput.context
304
+ );
305
+ if (complexity.tier === "low") {
306
+ const cheap = findAvailableCheapProvider();
307
+ if (cheap) return cheap;
308
+ }
309
+ if (complexity.tier === "high") {
310
+ return defaultResult;
311
+ }
312
+ }
313
+ const route = OPTIMAL_ROUTING[taskType];
314
+ if (route && process.env[route.apiKeyEnv]) {
315
+ return { ...route };
316
+ }
317
+ const fallbackConfig = loadModelRouterConfig();
318
+ for (const provider of FALLBACK_CHAIN) {
319
+ const providerConfig = fallbackConfig.providers[provider];
320
+ if (providerConfig && process.env[providerConfig.apiKeyEnv]) {
321
+ return {
322
+ provider,
323
+ model: providerConfig.model,
324
+ baseUrl: providerConfig.baseUrl,
325
+ apiKeyEnv: providerConfig.apiKeyEnv
326
+ };
327
+ }
328
+ }
329
+ return defaultResult;
330
+ }
331
+ function findAvailableCheapProvider() {
332
+ for (const p of CHEAP_PROVIDERS) {
333
+ if (process.env[p.apiKeyEnv]) {
334
+ return {
335
+ provider: p.provider,
336
+ model: p.model,
337
+ apiKeyEnv: p.apiKeyEnv,
338
+ baseUrl: p.baseUrl
339
+ };
340
+ }
341
+ }
342
+ return null;
343
+ }
344
+ function getComplexityRoutedProvider(taskType, task, context) {
345
+ const complexity = scoreComplexity(task, context);
346
+ const provider = getOptimalProvider(taskType, void 0, { task, context });
347
+ return { ...provider, complexity: complexity.tier };
348
+ }
127
349
  class ModelRouter {
128
350
  config;
129
351
  currentProvider;
@@ -196,28 +418,6 @@ class ModelRouter {
196
418
  const apiKey = process.env[fallbackProvider.apiKeyEnv];
197
419
  return !!apiKey;
198
420
  }
199
- /**
200
- * Check if error should trigger fallback
201
- */
202
- shouldFallback(error) {
203
- if (!this.isFallbackEnabled()) return false;
204
- if (this.inFallbackMode) return false;
205
- const fallback = this.config.fallback;
206
- if (fallback.onRateLimit && error.status === 429) {
207
- return true;
208
- }
209
- if (fallback.onError && error.status && error.status >= 500) {
210
- return true;
211
- }
212
- if (fallback.onTimeout) {
213
- const isTimeout = error.code === "ETIMEDOUT" || error.code === "ESOCKETTIMEDOUT" || error.message?.toLowerCase().includes("timeout");
214
- if (isTimeout) return true;
215
- }
216
- if (error.message?.toLowerCase().includes("overloaded")) {
217
- return true;
218
- }
219
- return false;
220
- }
221
421
  /**
222
422
  * Activate fallback mode
223
423
  */
@@ -278,16 +478,6 @@ function getModelRouter() {
278
478
  }
279
479
  return routerInstance;
280
480
  }
281
- function getPlanModeEnv() {
282
- const router = getModelRouter();
283
- const result = router.route("plan");
284
- return result.env;
285
- }
286
- function getThinkingModeEnv() {
287
- const router = getModelRouter();
288
- const result = router.route("think");
289
- return result.env;
290
- }
291
481
  function isFallbackAvailable() {
292
482
  const router = getModelRouter();
293
483
  return router.isFallbackEnabled();
@@ -322,13 +512,17 @@ function resetFallback() {
322
512
  router.reset();
323
513
  }
324
514
  export {
515
+ DEFAULT_MODEL_TOKEN_LIMIT,
516
+ MODEL_TOKEN_LIMITS,
325
517
  ModelRouter,
326
518
  buildModelEnv,
519
+ getComplexityRoutedProvider,
327
520
  getFallbackStatus,
328
521
  getModelForTask,
329
522
  getModelRouter,
330
- getPlanModeEnv,
331
- getThinkingModeEnv,
523
+ getModelTokenLimit,
524
+ getOptimalProvider,
525
+ invalidateConfigCache,
332
526
  isFallbackAvailable,
333
527
  isPlanningContext,
334
528
  loadModelRouterConfig,
@@ -0,0 +1,63 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ const MODEL_PRICING = {
6
+ // Anthropic (direct API)
7
+ "anthropic/claude-sonnet-4-5-20250929": {
8
+ inputPer1M: 3,
9
+ outputPer1M: 15,
10
+ source: "anthropic.com"
11
+ },
12
+ "anthropic/claude-sonnet-4-20250514": {
13
+ inputPer1M: 3,
14
+ outputPer1M: 15,
15
+ source: "anthropic.com"
16
+ },
17
+ "anthropic/claude-haiku-4-5-20251001": {
18
+ inputPer1M: 0.8,
19
+ outputPer1M: 4,
20
+ source: "anthropic.com"
21
+ },
22
+ // OpenAI (direct API)
23
+ "openai/gpt-4o": {
24
+ inputPer1M: 2.5,
25
+ outputPer1M: 10,
26
+ source: "openai.com"
27
+ },
28
+ // OpenRouter (aggregated)
29
+ "openrouter/meta-llama/llama-4-scout": {
30
+ inputPer1M: 0.08,
31
+ outputPer1M: 0.3,
32
+ source: "openrouter.ai/api/v1/models"
33
+ },
34
+ // Cerebras (free tier / inference)
35
+ "cerebras/llama-4-scout-17b-16e-instruct": {
36
+ inputPer1M: 0.1,
37
+ outputPer1M: 0.1,
38
+ source: "cerebras.ai"
39
+ },
40
+ // DeepInfra
41
+ "deepinfra/THUDM/glm-4-9b-chat": {
42
+ inputPer1M: 0.065,
43
+ outputPer1M: 0.065,
44
+ source: "deepinfra.com"
45
+ }
46
+ };
47
+ function calculateCost(provider, model, inputTokens, outputTokens) {
48
+ const key = `${provider}/${model}`;
49
+ const pricing = MODEL_PRICING[key];
50
+ if (!pricing) return null;
51
+ const inputCost = inputTokens / 1e6 * pricing.inputPer1M;
52
+ const outputCost = outputTokens / 1e6 * pricing.outputPer1M;
53
+ return { inputCost, outputCost, totalCost: inputCost + outputCost };
54
+ }
55
+ function formatCost(usd) {
56
+ if (usd < 0.01) return `$${usd.toFixed(6)}`;
57
+ return `$${usd.toFixed(4)}`;
58
+ }
59
+ export {
60
+ MODEL_PRICING,
61
+ calculateCost,
62
+ formatCost
63
+ };
@@ -0,0 +1,112 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ const SENSITIVE_PATTERNS = [
6
+ // API keys — common prefixes
7
+ { pattern: /\bsk-[a-zA-Z0-9]{20,}/, label: "API key (sk-)" },
8
+ { pattern: /\bsk-ant-[a-zA-Z0-9-]{20,}/, label: "Anthropic key" },
9
+ { pattern: /\bAKIA[A-Z0-9]{12,}/, label: "AWS access key" },
10
+ { pattern: /\bghp_[a-zA-Z0-9]{30,}/, label: "GitHub PAT" },
11
+ { pattern: /\bgho_[a-zA-Z0-9]{30,}/, label: "GitHub OAuth" },
12
+ { pattern: /\bghs_[a-zA-Z0-9]{30,}/, label: "GitHub App" },
13
+ { pattern: /\bglpat-[a-zA-Z0-9_-]{20,}/, label: "GitLab PAT" },
14
+ { pattern: /\bnpm_[a-zA-Z0-9]{30,}/, label: "npm token" },
15
+ { pattern: /\bxox[bpsar]-[a-zA-Z0-9-]{10,}/, label: "Slack token" },
16
+ { pattern: /\blin_api_[a-zA-Z0-9]{20,}/, label: "Linear API key" },
17
+ { pattern: /\blin_oauth_[a-zA-Z0-9]{20,}/, label: "Linear OAuth" },
18
+ { pattern: /\bSG\.[a-zA-Z0-9_-]{20,}/, label: "SendGrid key" },
19
+ { pattern: /\brk_live_[a-zA-Z0-9]{20,}/, label: "Stripe key" },
20
+ { pattern: /\bsk_(?:live|test)_[a-zA-Z0-9]{20,}/, label: "Stripe secret" },
21
+ { pattern: /\bwhsec_[a-zA-Z0-9]{20,}/, label: "Webhook secret" },
22
+ // Private keys and certificates
23
+ {
24
+ pattern: /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----/,
25
+ label: "Private key (PEM)"
26
+ },
27
+ { pattern: /-----BEGIN\s+CERTIFICATE-----/, label: "Certificate (PEM)" },
28
+ {
29
+ pattern: /-----BEGIN\s+(?:EC\s+)?PRIVATE\s+KEY-----/,
30
+ label: "EC private key"
31
+ },
32
+ // JWT tokens (header.payload.signature format)
33
+ {
34
+ pattern: /\beyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/,
35
+ label: "JWT token"
36
+ },
37
+ { pattern: /\bBearer\s+eyJ[a-zA-Z0-9_-]{10,}/, label: "Bearer JWT" },
38
+ // Connection strings with credentials
39
+ {
40
+ pattern: /(?:mysql|postgres|postgresql|mongodb|redis):\/\/[^:]+:[^@]+@/,
41
+ label: "DB connection string"
42
+ },
43
+ // Generic high-entropy secrets (base64 blocks in config-like context)
44
+ {
45
+ pattern: /(?:password|secret|token|api[_-]?key|auth[_-]?token)\s*[:=]\s*["'][^"']{8,}["']/i,
46
+ label: "Credential assignment"
47
+ },
48
+ {
49
+ pattern: /(?:PASSWORD|SECRET|TOKEN|API_KEY|AUTH_TOKEN)\s*=\s*\S{8,}/,
50
+ label: "Env var credential"
51
+ }
52
+ ];
53
+ const APPROVED_PROVIDERS = /* @__PURE__ */ new Set(["anthropic", "anthropic-batch"]);
54
+ function checkString(text) {
55
+ const matches = [];
56
+ for (const { pattern, label } of SENSITIVE_PATTERNS) {
57
+ if (pattern.test(text)) {
58
+ matches.push(label);
59
+ }
60
+ }
61
+ return { sensitive: matches.length > 0, matches };
62
+ }
63
+ function detectSensitiveContent(task, context) {
64
+ const allMatches = [];
65
+ const taskResult = checkString(task);
66
+ allMatches.push(...taskResult.matches);
67
+ if (context) {
68
+ for (const value of Object.values(context)) {
69
+ if (typeof value === "string") {
70
+ const r = checkString(value);
71
+ allMatches.push(...r.matches);
72
+ } else if (Array.isArray(value)) {
73
+ for (const item of value) {
74
+ if (typeof item === "string") {
75
+ const r = checkString(item);
76
+ allMatches.push(...r.matches);
77
+ }
78
+ }
79
+ } else if (value && typeof value === "object") {
80
+ for (const nested of Object.values(value)) {
81
+ if (typeof nested === "string") {
82
+ const r = checkString(nested);
83
+ allMatches.push(...r.matches);
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+ const unique = [...new Set(allMatches)];
90
+ return { sensitive: unique.length > 0, matches: unique };
91
+ }
92
+ function isApprovedProvider(provider) {
93
+ return APPROVED_PROVIDERS.has(provider);
94
+ }
95
+ function shouldBlockProvider(provider, task, context) {
96
+ if (isApprovedProvider(provider)) {
97
+ return { blocked: false };
98
+ }
99
+ const check = detectSensitiveContent(task, context);
100
+ if (!check.sensitive) {
101
+ return { blocked: false };
102
+ }
103
+ return {
104
+ blocked: true,
105
+ reason: `Sensitive content detected (${check.matches.join(", ")}). Blocked routing to ${provider}; using approved provider instead.`
106
+ };
107
+ }
108
+ export {
109
+ detectSensitiveContent,
110
+ isApprovedProvider,
111
+ shouldBlockProvider
112
+ };
@@ -0,0 +1,88 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import { EventEmitter } from "events";
6
+ const DEFAULT_CONFIG = {
7
+ contextPressure: { enabled: true, cooldownSec: 60 },
8
+ editRecovery: { enabled: true, cooldownSec: 0 },
9
+ retrievalQuality: { enabled: true, cooldownSec: 300 },
10
+ traceErrorChain: { enabled: true, cooldownSec: 30 },
11
+ harnessRegression: { enabled: true, cooldownSec: 0 },
12
+ sessionDrift: { enabled: true, cooldownSec: 120 }
13
+ };
14
+ class FeedbackLoopEngine extends EventEmitter {
15
+ config;
16
+ lastFired = /* @__PURE__ */ new Map();
17
+ history = [];
18
+ maxHistory = 200;
19
+ constructor(config = {}) {
20
+ super();
21
+ this.config = { ...DEFAULT_CONFIG, ...config };
22
+ }
23
+ /**
24
+ * Fire a loop if enabled and not in cooldown.
25
+ * Returns the LoopEvent if fired, null if skipped.
26
+ */
27
+ fire(loopName, trigger, data, action, outcome = "success") {
28
+ const cfg = this.config[loopName];
29
+ if (!cfg?.enabled) return null;
30
+ const now = Date.now();
31
+ const lastTime = this.lastFired.get(loopName) || 0;
32
+ if (now - lastTime < cfg.cooldownSec * 1e3) return null;
33
+ const event = {
34
+ loop: loopName,
35
+ trigger,
36
+ timestamp: now,
37
+ data,
38
+ action,
39
+ outcome
40
+ };
41
+ this.lastFired.set(loopName, now);
42
+ this.history.push(event);
43
+ if (this.history.length > this.maxHistory) {
44
+ this.history = this.history.slice(-this.maxHistory);
45
+ }
46
+ this.emit("loop", event);
47
+ this.emit(`loop:${loopName}`, event);
48
+ return event;
49
+ }
50
+ /** Get recent loop events, optionally filtered by loop name. */
51
+ getHistory(loopName, limit = 50) {
52
+ const filtered = loopName ? this.history.filter((e) => e.loop === loopName) : this.history;
53
+ return filtered.slice(-limit);
54
+ }
55
+ /** Get summary stats per loop. */
56
+ getStats() {
57
+ const stats = {};
58
+ for (const event of this.history) {
59
+ if (!stats[event.loop]) {
60
+ stats[event.loop] = {
61
+ fires: 0,
62
+ successes: 0,
63
+ errors: 0,
64
+ lastFired: null
65
+ };
66
+ }
67
+ stats[event.loop].fires++;
68
+ if (event.outcome === "success") stats[event.loop].successes++;
69
+ if (event.outcome === "error") stats[event.loop].errors++;
70
+ stats[event.loop].lastFired = event.timestamp;
71
+ }
72
+ return stats;
73
+ }
74
+ /** Update config at runtime. */
75
+ updateConfig(partial) {
76
+ this.config = { ...this.config, ...partial };
77
+ }
78
+ /** Get current config. */
79
+ getConfig() {
80
+ return { ...this.config };
81
+ }
82
+ }
83
+ const feedbackLoops = new FeedbackLoopEngine();
84
+ export {
85
+ DEFAULT_CONFIG,
86
+ FeedbackLoopEngine,
87
+ feedbackLoops
88
+ };
@@ -16,6 +16,10 @@ const ModelProviderSchema = z.enum([
16
16
  "qwen",
17
17
  "openai",
18
18
  "ollama",
19
+ "cerebras",
20
+ "deepinfra",
21
+ "openrouter",
22
+ "anthropic-batch",
19
23
  "custom"
20
24
  ]);
21
25
  const ModelConfigSchema = z.object({
@@ -33,7 +37,10 @@ const ModelRouterConfigSchema = z.object({
33
37
  plan: ModelProviderSchema.optional(),
34
38
  think: ModelProviderSchema.optional(),
35
39
  code: ModelProviderSchema.optional(),
36
- review: ModelProviderSchema.optional()
40
+ review: ModelProviderSchema.optional(),
41
+ linting: ModelProviderSchema.optional(),
42
+ context: ModelProviderSchema.optional(),
43
+ testing: ModelProviderSchema.optional()
37
44
  }).optional().default({}),
38
45
  fallback: z.object({
39
46
  enabled: z.boolean(),
@@ -49,6 +56,10 @@ const ModelRouterConfigSchema = z.object({
49
56
  qwen: ModelConfigSchema.optional(),
50
57
  openai: ModelConfigSchema.optional(),
51
58
  ollama: ModelConfigSchema.optional(),
59
+ cerebras: ModelConfigSchema.optional(),
60
+ deepinfra: ModelConfigSchema.optional(),
61
+ openrouter: ModelConfigSchema.optional(),
62
+ "anthropic-batch": ModelConfigSchema.optional(),
52
63
  custom: ModelConfigSchema.optional()
53
64
  }).optional().default({}),
54
65
  thinkingMode: z.object({