@stackmemoryai/stackmemory 1.0.0 → 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 (39) hide show
  1. package/README.md +66 -270
  2. package/dist/src/cli/claude-sm.js +9 -0
  3. package/dist/src/cli/codex-sm.js +9 -0
  4. package/dist/src/cli/commands/audit.js +134 -0
  5. package/dist/src/cli/commands/bench.js +252 -0
  6. package/dist/src/cli/commands/dashboard.js +2 -1
  7. package/dist/src/cli/commands/stats.js +118 -0
  8. package/dist/src/cli/index.js +6 -0
  9. package/dist/src/core/config/feature-flags.js +7 -1
  10. package/dist/src/core/context/enhanced-rehydration.js +24 -5
  11. package/dist/src/core/extensions/cerebras-adapter.js +28 -0
  12. package/dist/src/core/extensions/deepinfra-adapter.js +32 -0
  13. package/dist/src/core/extensions/provider-adapter.js +33 -240
  14. package/dist/src/core/models/complexity-scorer.js +154 -0
  15. package/dist/src/core/models/model-router.js +230 -36
  16. package/dist/src/core/models/provider-pricing.js +63 -0
  17. package/dist/src/core/models/sensitive-guard.js +112 -0
  18. package/dist/src/core/monitoring/feedback-loops.js +88 -0
  19. package/dist/src/features/sweep/pty-wrapper.js +9 -0
  20. package/dist/src/hooks/daemon.js +8 -0
  21. package/dist/src/hooks/graphiti-hooks.js +104 -0
  22. package/dist/src/hooks/schemas.js +12 -1
  23. package/dist/src/integrations/anthropic/batch-client.js +256 -0
  24. package/dist/src/integrations/anthropic/client.js +87 -72
  25. package/dist/src/integrations/claude-code/subagent-client.js +133 -12
  26. package/dist/src/integrations/graphiti/client.js +115 -0
  27. package/dist/src/integrations/graphiti/config.js +17 -0
  28. package/dist/src/integrations/graphiti/types.js +4 -0
  29. package/dist/src/integrations/mcp/handlers/index.js +25 -1
  30. package/dist/src/integrations/mcp/handlers/provider-handlers.js +227 -0
  31. package/dist/src/integrations/mcp/server.js +207 -1
  32. package/dist/src/integrations/mcp/tool-definitions.js +38 -1
  33. package/dist/src/orchestrators/multimodal/baselines.js +128 -0
  34. package/dist/src/orchestrators/multimodal/constants.js +9 -1
  35. package/dist/src/orchestrators/multimodal/harness.js +86 -6
  36. package/dist/src/orchestrators/multimodal/providers.js +113 -2
  37. package/dist/src/skills/recursive-agent-orchestrator.js +15 -10
  38. package/dist/src/utils/fuzzy-edit.js +162 -0
  39. package/package.json +5 -1
@@ -3,11 +3,21 @@ import { dirname as __pathDirname } from 'path';
3
3
  const __filename = __fileURLToPath(import.meta.url);
4
4
  const __dirname = __pathDirname(__filename);
5
5
  import { logger } from "../../core/monitoring/logger.js";
6
+ import { STRUCTURED_RESPONSE_SUFFIX } from "../../orchestrators/multimodal/constants.js";
6
7
  import { exec } from "child_process";
7
8
  import { promisify } from "util";
8
9
  import * as fs from "fs";
9
10
  import * as path from "path";
10
11
  import * as os from "os";
12
+ import { isFeatureEnabled } from "../../core/config/feature-flags.js";
13
+ import {
14
+ getOptimalProvider
15
+ } from "../../core/models/model-router.js";
16
+ import { scoreComplexity } from "../../core/models/complexity-scorer.js";
17
+ import {
18
+ createProvider
19
+ } from "../../core/extensions/provider-adapter.js";
20
+ import { AnthropicBatchClient } from "../anthropic/batch-client.js";
11
21
  const execAsync = promisify(exec);
12
22
  class ClaudeCodeSubagentClient {
13
23
  tempDir;
@@ -25,8 +35,9 @@ class ClaudeCodeSubagentClient {
25
35
  });
26
36
  }
27
37
  /**
28
- * Execute a subagent task using Claude Code's Task tool
29
- * This will spawn a new Claude instance with specific instructions
38
+ * Execute a subagent task.
39
+ * When multiProvider is enabled, routes cheap tasks to external providers.
40
+ * Falls back to Claude Code CLI path when disabled or for complex tasks.
30
41
  */
31
42
  async executeSubagent(request) {
32
43
  const startTime = Date.now();
@@ -39,6 +50,107 @@ class ClaudeCodeSubagentClient {
39
50
  if (this.mockMode) {
40
51
  return this.getMockResponse(request, startTime, subagentId);
41
52
  }
53
+ if (isFeatureEnabled("multiProvider")) {
54
+ const taskType = request.type;
55
+ const complexity = scoreComplexity(request.task, request.context);
56
+ const optimal = getOptimalProvider(taskType, void 0, {
57
+ task: request.task,
58
+ context: request.context
59
+ });
60
+ logger.debug("Complexity-based routing", {
61
+ subagentId,
62
+ taskType,
63
+ complexity: complexity.tier,
64
+ score: complexity.score,
65
+ signals: complexity.signals,
66
+ routed: optimal.provider
67
+ });
68
+ if (optimal.provider !== "anthropic" && optimal.provider !== "anthropic-batch") {
69
+ return this.executeDirectAPI(request, optimal, startTime, subagentId);
70
+ }
71
+ if (optimal.provider === "anthropic-batch") {
72
+ return this.executeBatch(request, startTime, subagentId);
73
+ }
74
+ }
75
+ return this.executeSubagentViaCLI(request, startTime, subagentId);
76
+ }
77
+ /**
78
+ * Execute via external provider (Cerebras, DeepInfra, etc.) using OpenAI-compatible API
79
+ */
80
+ async executeDirectAPI(request, optimal, startTime, subagentId) {
81
+ try {
82
+ const apiKey = process.env[optimal.apiKeyEnv] || "";
83
+ if (!apiKey) {
84
+ logger.warn(
85
+ `No API key for ${optimal.provider}, falling back to Claude`
86
+ );
87
+ return this.executeSubagentViaCLI(request, startTime, subagentId);
88
+ }
89
+ const adapter = createProvider(optimal.provider, {
90
+ apiKey,
91
+ baseUrl: optimal.baseUrl
92
+ });
93
+ const prompt = this.buildSubagentPrompt(request);
94
+ const result = await adapter.complete(
95
+ [{ role: "user", content: prompt }],
96
+ { model: optimal.model, maxTokens: 4096 }
97
+ );
98
+ const text = result.content.filter((c) => c.type === "text").map((c) => c.text).join("");
99
+ let parsed;
100
+ try {
101
+ parsed = JSON.parse(text);
102
+ } catch {
103
+ parsed = { rawOutput: text };
104
+ }
105
+ return {
106
+ success: true,
107
+ result: parsed,
108
+ output: text,
109
+ duration: Date.now() - startTime,
110
+ subagentType: request.type,
111
+ tokens: result.usage.inputTokens + result.usage.outputTokens
112
+ };
113
+ } catch (error) {
114
+ logger.warn(`Direct API failed for ${optimal.provider}, falling back`, {
115
+ error: error.message
116
+ });
117
+ return this.executeSubagentViaCLI(request, startTime, subagentId);
118
+ }
119
+ }
120
+ /**
121
+ * Execute via Anthropic Batch API (async, 50% discount)
122
+ */
123
+ async executeBatch(request, startTime, subagentId) {
124
+ try {
125
+ const batchClient = new AnthropicBatchClient();
126
+ const prompt = this.buildSubagentPrompt(request);
127
+ const batchReq = {
128
+ custom_id: subagentId,
129
+ params: {
130
+ model: "claude-sonnet-4-5-20250929",
131
+ max_tokens: 4096,
132
+ messages: [{ role: "user", content: prompt }]
133
+ }
134
+ };
135
+ const batchId = await batchClient.submit([batchReq], request.type);
136
+ return {
137
+ success: true,
138
+ result: { batchId, status: "submitted", custom_id: subagentId },
139
+ output: `Batch submitted: ${batchId}`,
140
+ duration: Date.now() - startTime,
141
+ subagentType: request.type
142
+ };
143
+ } catch (error) {
144
+ logger.warn("Batch submit failed, falling back to sync", {
145
+ error: error.message
146
+ });
147
+ return this.executeSubagentViaCLI(request, startTime, subagentId);
148
+ }
149
+ }
150
+ /**
151
+ * Original CLI-based subagent execution (unchanged behavior)
152
+ */
153
+ async executeSubagentViaCLI(request, startTime, subagentId) {
42
154
  try {
43
155
  const prompt = this.buildSubagentPrompt(request);
44
156
  const contextFile = path.join(this.tempDir, `${subagentId}-context.json`);
@@ -47,14 +159,19 @@ class ClaudeCodeSubagentClient {
47
159
  JSON.stringify(request.context, null, 2)
48
160
  );
49
161
  const resultFile = path.join(this.tempDir, `${subagentId}-result.json`);
50
- const taskCommand = this.buildTaskCommand(request, prompt, contextFile, resultFile);
162
+ const taskCommand = this.buildTaskCommand(
163
+ request,
164
+ prompt,
165
+ contextFile,
166
+ resultFile
167
+ );
51
168
  const result = await this.executeTaskTool(taskCommand, request.timeout);
52
169
  let subagentResult = {};
53
170
  if (fs.existsSync(resultFile)) {
54
171
  const resultContent = await fs.promises.readFile(resultFile, "utf-8");
55
172
  try {
56
173
  subagentResult = JSON.parse(resultContent);
57
- } catch (e) {
174
+ } catch {
58
175
  subagentResult = { rawOutput: resultContent };
59
176
  }
60
177
  }
@@ -68,7 +185,10 @@ class ClaudeCodeSubagentClient {
68
185
  tokens: this.estimateTokens(prompt + JSON.stringify(subagentResult))
69
186
  };
70
187
  } catch (error) {
71
- logger.error(`Subagent execution failed: ${request.type}`, { error, subagentId });
188
+ logger.error(`Subagent CLI execution failed: ${request.type}`, {
189
+ error,
190
+ subagentId
191
+ });
72
192
  return {
73
193
  success: false,
74
194
  result: null,
@@ -224,7 +344,7 @@ class ClaudeCodeSubagentClient {
224
344
 
225
345
  Output the release plan and commands.`
226
346
  };
227
- return request.systemPrompt || prompts[request.type] || prompts.planning;
347
+ return (request.systemPrompt || prompts[request.type] || prompts.planning) + STRUCTURED_RESPONSE_SUFFIX;
228
348
  }
229
349
  /**
230
350
  * Build Task tool command
@@ -292,7 +412,9 @@ echo '{"status": "completed", "type": "${request.type}"}' > "${resultFile}"
292
412
  * Get mock response for testing
293
413
  */
294
414
  async getMockResponse(request, startTime, subagentId) {
295
- await new Promise((resolve) => setTimeout(resolve, Math.random() * 20 + 10));
415
+ await new Promise(
416
+ (resolve) => setTimeout(resolve, Math.random() * 20 + 10)
417
+ );
296
418
  const mockResponses = {
297
419
  planning: {
298
420
  tasks: [
@@ -357,10 +479,7 @@ function greetUser(name: string): string {
357
479
  }
358
480
  return \`Hello, \${name}!\`;
359
481
  }`,
360
- changes_made: [
361
- "Added JSDoc documentation",
362
- "Improved error message"
363
- ]
482
+ changes_made: ["Added JSDoc documentation", "Improved error message"]
364
483
  },
365
484
  context: {
366
485
  relevant_files: ["src/greet.ts", "test/greet.test.ts"],
@@ -415,7 +534,9 @@ function greetUser(name: string): string {
415
534
  */
416
535
  async mockTaskToolExecution(request) {
417
536
  const startTime = Date.now();
418
- await new Promise((resolve) => setTimeout(resolve, 1e3 + Math.random() * 2e3));
537
+ await new Promise(
538
+ (resolve) => setTimeout(resolve, 1e3 + Math.random() * 2e3)
539
+ );
419
540
  const mockResponses = {
420
541
  planning: {
421
542
  tasks: [
@@ -0,0 +1,115 @@
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 { DEFAULT_GRAPHITI_CONFIG } from "./config.js";
6
+ class GraphitiClientError extends Error {
7
+ constructor(message, code, statusCode) {
8
+ super(message);
9
+ this.code = code;
10
+ this.statusCode = statusCode;
11
+ this.name = "GraphitiClientError";
12
+ }
13
+ }
14
+ class GraphitiClient {
15
+ endpoint;
16
+ timeout;
17
+ maxRetries;
18
+ namespace;
19
+ constructor(config = {}) {
20
+ const merged = { ...DEFAULT_GRAPHITI_CONFIG, ...config };
21
+ this.endpoint = merged.endpoint.replace(/\/$/, "");
22
+ this.timeout = merged.timeoutMs;
23
+ this.maxRetries = merged.maxRetries;
24
+ this.namespace = merged.projectNamespace || "default";
25
+ }
26
+ async request(path, options = {}) {
27
+ const controller = new AbortController();
28
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
29
+ let lastError;
30
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
31
+ try {
32
+ const res = await fetch(`${this.endpoint}${path}`, {
33
+ ...options,
34
+ signal: controller.signal,
35
+ headers: {
36
+ "Content-Type": "application/json",
37
+ ...options.headers || {}
38
+ }
39
+ });
40
+ clearTimeout(timeoutId);
41
+ if (!res.ok) {
42
+ const msg = await res.text().catch(() => res.statusText);
43
+ throw new GraphitiClientError(
44
+ `Request failed: ${msg}`,
45
+ "HTTP_ERROR",
46
+ res.status
47
+ );
48
+ }
49
+ return await res.json();
50
+ } catch (err) {
51
+ lastError = err;
52
+ if (err instanceof GraphitiClientError) throw err;
53
+ if (err.name === "AbortError") {
54
+ throw new GraphitiClientError("Request timeout", "TIMEOUT");
55
+ }
56
+ if (attempt < this.maxRetries) {
57
+ await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 100));
58
+ continue;
59
+ }
60
+ }
61
+ }
62
+ clearTimeout(timeoutId);
63
+ throw new GraphitiClientError(
64
+ lastError?.message || "Network error",
65
+ "NETWORK_ERROR"
66
+ );
67
+ }
68
+ // Episodes
69
+ async upsertEpisode(episode) {
70
+ const payload = { ...episode, namespace: this.namespace };
71
+ const res = await this.request(`/episodes`, {
72
+ method: "POST",
73
+ body: JSON.stringify(payload)
74
+ });
75
+ return res;
76
+ }
77
+ // Entities
78
+ async upsertEntities(entities) {
79
+ const payload = { entities, namespace: this.namespace };
80
+ return this.request(`/entities:batchUpsert`, {
81
+ method: "POST",
82
+ body: JSON.stringify(payload)
83
+ });
84
+ }
85
+ // Relations
86
+ async upsertRelations(edges) {
87
+ const payload = { edges, namespace: this.namespace };
88
+ return this.request(`/relations:batchUpsert`, {
89
+ method: "POST",
90
+ body: JSON.stringify(payload)
91
+ });
92
+ }
93
+ // Temporal query + hybrid retrieval
94
+ async queryTemporal(query) {
95
+ const payload = { ...query, namespace: this.namespace };
96
+ return this.request(`/query/temporal`, {
97
+ method: "POST",
98
+ body: JSON.stringify(payload)
99
+ });
100
+ }
101
+ // Health/status
102
+ async getStatus() {
103
+ try {
104
+ return await this.request(
105
+ `/status?namespace=${encodeURIComponent(this.namespace)}`
106
+ );
107
+ } catch {
108
+ return { connected: false };
109
+ }
110
+ }
111
+ }
112
+ export {
113
+ GraphitiClient,
114
+ GraphitiClientError
115
+ };
@@ -0,0 +1,17 @@
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 DEFAULT_GRAPHITI_CONFIG = {
6
+ enabled: !!process.env.GRAPHITI_ENDPOINT,
7
+ endpoint: process.env.GRAPHITI_ENDPOINT?.replace(/\/$/, "") || "http://localhost:8080",
8
+ backend: process.env.GRAPHITI_BACKEND || "neo4j",
9
+ projectNamespace: process.env.STACKMEMORY_PROJECT_ID || "default",
10
+ timeoutMs: 5e3,
11
+ maxRetries: 2,
12
+ maxTokens: 1600,
13
+ maxHops: 2
14
+ };
15
+ export {
16
+ DEFAULT_GRAPHITI_CONFIG
17
+ };
@@ -0,0 +1,4 @@
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);
@@ -15,6 +15,9 @@ import {
15
15
  import {
16
16
  DiscoveryHandlers
17
17
  } from "./discovery-handlers.js";
18
+ import {
19
+ ProviderHandlers
20
+ } from "./provider-handlers.js";
18
21
  import {
19
22
  ContextHandlers as ContextHandlers2
20
23
  } from "./context-handlers.js";
@@ -23,11 +26,13 @@ import {
23
26
  LinearHandlers as LinearHandlers2
24
27
  } from "./linear-handlers.js";
25
28
  import { TraceHandlers as TraceHandlers2 } from "./trace-handlers.js";
29
+ import { ProviderHandlers as ProviderHandlers2 } from "./provider-handlers.js";
26
30
  class MCPHandlerFactory {
27
31
  contextHandlers;
28
32
  taskHandlers;
29
33
  linearHandlers;
30
34
  traceHandlers;
35
+ providerHandlers;
31
36
  constructor(deps) {
32
37
  this.contextHandlers = new ContextHandlers2({
33
38
  frameManager: deps.frameManager
@@ -45,6 +50,7 @@ class MCPHandlerFactory {
45
50
  traceDetector: deps.traceDetector,
46
51
  browserMCP: deps.browserMCP
47
52
  });
53
+ this.providerHandlers = new ProviderHandlers2();
48
54
  }
49
55
  /**
50
56
  * Get handler for a specific tool
@@ -111,6 +117,19 @@ class MCPHandlerFactory {
111
117
  return this.traceHandlers.handleStopBrowserDebug.bind(
112
118
  this.traceHandlers
113
119
  );
120
+ // Provider handlers
121
+ case "delegate_to_model":
122
+ return this.providerHandlers.handleDelegateToModel.bind(
123
+ this.providerHandlers
124
+ );
125
+ case "batch_submit":
126
+ return this.providerHandlers.handleBatchSubmit.bind(
127
+ this.providerHandlers
128
+ );
129
+ case "batch_check":
130
+ return this.providerHandlers.handleBatchCheck.bind(
131
+ this.providerHandlers
132
+ );
114
133
  default:
115
134
  throw new Error(`Unknown tool: ${toolName}`);
116
135
  }
@@ -144,7 +163,11 @@ class MCPHandlerFactory {
144
163
  "start_browser_debug",
145
164
  "take_screenshot",
146
165
  "execute_script",
147
- "stop_browser_debug"
166
+ "stop_browser_debug",
167
+ // Provider tools (conditionally active)
168
+ "delegate_to_model",
169
+ "batch_submit",
170
+ "batch_check"
148
171
  ];
149
172
  }
150
173
  /**
@@ -159,6 +182,7 @@ export {
159
182
  DiscoveryHandlers,
160
183
  LinearHandlers,
161
184
  MCPHandlerFactory,
185
+ ProviderHandlers,
162
186
  TaskHandlers,
163
187
  TraceHandlers
164
188
  };
@@ -0,0 +1,227 @@
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 { logger } from "../../../core/monitoring/logger.js";
6
+ import { isFeatureEnabled } from "../../../core/config/feature-flags.js";
7
+ import {
8
+ createProvider
9
+ } from "../../../core/extensions/provider-adapter.js";
10
+ import {
11
+ getOptimalProvider
12
+ } from "../../../core/models/model-router.js";
13
+ import { scoreComplexity } from "../../../core/models/complexity-scorer.js";
14
+ import {
15
+ AnthropicBatchClient
16
+ } from "../../anthropic/batch-client.js";
17
+ function errorResponse(err) {
18
+ return {
19
+ content: [{ type: "text", text: JSON.stringify(err, null, 2) }]
20
+ };
21
+ }
22
+ function classifyApiError(error, provider) {
23
+ const msg = error.message || String(error);
24
+ const status = error.status || (msg.match(/(\d{3})/)?.[1] ? parseInt(msg.match(/(\d{3})/)[1]) : void 0);
25
+ if (status === 429) {
26
+ return {
27
+ errorType: "rate_limit",
28
+ message: msg,
29
+ recommendation: `Rate limited by ${provider}. Retry after a delay or switch to a different provider.`,
30
+ provider
31
+ };
32
+ }
33
+ if (status && status >= 500) {
34
+ return {
35
+ errorType: "server_error",
36
+ message: msg,
37
+ recommendation: `${provider} returned a server error. Try a different provider or retry later.`,
38
+ provider
39
+ };
40
+ }
41
+ return {
42
+ errorType: "api_error",
43
+ message: msg,
44
+ recommendation: `API call to ${provider} failed. Check the model name, API key, and base URL.`,
45
+ provider
46
+ };
47
+ }
48
+ class ProviderHandlers {
49
+ batchClient;
50
+ constructor(_deps) {
51
+ }
52
+ getBatchClient() {
53
+ if (!this.batchClient) {
54
+ this.batchClient = new AnthropicBatchClient();
55
+ }
56
+ return this.batchClient;
57
+ }
58
+ /**
59
+ * delegate_to_model — route a prompt to a specific provider+model
60
+ */
61
+ async handleDelegateToModel(args) {
62
+ if (!isFeatureEnabled("multiProvider")) {
63
+ return errorResponse({
64
+ errorType: "feature_disabled",
65
+ message: "Multi-provider routing is disabled.",
66
+ recommendation: "Set STACKMEMORY_MULTI_PROVIDER=true to enable multi-provider routing."
67
+ });
68
+ }
69
+ const taskType = args.taskType || "default";
70
+ const preference = args.provider;
71
+ const complexity = scoreComplexity(args.prompt);
72
+ const optimal = getOptimalProvider(taskType, preference, {
73
+ task: args.prompt
74
+ });
75
+ logger.info("delegate_to_model routing", {
76
+ taskType,
77
+ complexity: complexity.tier,
78
+ score: complexity.score,
79
+ provider: optimal.provider
80
+ });
81
+ const providerModel = args.model || optimal.model;
82
+ const apiKey = process.env[optimal.apiKeyEnv] || "";
83
+ if (!apiKey) {
84
+ return errorResponse({
85
+ errorType: "missing_api_key",
86
+ message: `No API key found for ${optimal.provider} (env: ${optimal.apiKeyEnv})`,
87
+ recommendation: `Set the ${optimal.apiKeyEnv} environment variable or choose a different provider.`,
88
+ provider: optimal.provider
89
+ });
90
+ }
91
+ try {
92
+ const adapter = createProvider(optimal.provider, {
93
+ apiKey,
94
+ baseUrl: optimal.baseUrl
95
+ });
96
+ const messages = [{ role: "user", content: args.prompt }];
97
+ const result = await adapter.complete(messages, {
98
+ model: providerModel,
99
+ maxTokens: args.maxTokens || 4096,
100
+ temperature: args.temperature,
101
+ system: args.system
102
+ });
103
+ const text = result.content.filter((c) => c.type === "text").map((c) => c.text).join("");
104
+ return {
105
+ content: [
106
+ {
107
+ type: "text",
108
+ text: JSON.stringify(
109
+ {
110
+ provider: optimal.provider,
111
+ model: providerModel,
112
+ response: text,
113
+ usage: result.usage
114
+ },
115
+ null,
116
+ 2
117
+ )
118
+ }
119
+ ]
120
+ };
121
+ } catch (error) {
122
+ logger.error("delegate_to_model failed", { error: error.message });
123
+ return errorResponse(classifyApiError(error, optimal.provider));
124
+ }
125
+ }
126
+ /**
127
+ * batch_submit — submit prompts to Anthropic Batch API
128
+ */
129
+ async handleBatchSubmit(args) {
130
+ if (!isFeatureEnabled("multiProvider")) {
131
+ return errorResponse({
132
+ errorType: "feature_disabled",
133
+ message: "Multi-provider routing is disabled.",
134
+ recommendation: "Set STACKMEMORY_MULTI_PROVIDER=true to enable multi-provider routing."
135
+ });
136
+ }
137
+ try {
138
+ const batchClient = this.getBatchClient();
139
+ const requests = args.prompts.map((p) => ({
140
+ custom_id: p.id,
141
+ params: {
142
+ model: p.model || "claude-sonnet-4-5-20250929",
143
+ max_tokens: p.maxTokens || 4096,
144
+ messages: [{ role: "user", content: p.prompt }],
145
+ system: p.system
146
+ }
147
+ }));
148
+ const batchId = await batchClient.submit(requests, args.description);
149
+ return {
150
+ content: [
151
+ {
152
+ type: "text",
153
+ text: JSON.stringify(
154
+ {
155
+ batchId,
156
+ status: "submitted",
157
+ requestCount: requests.length
158
+ },
159
+ null,
160
+ 2
161
+ )
162
+ }
163
+ ]
164
+ };
165
+ } catch (error) {
166
+ return errorResponse({
167
+ errorType: "batch_error",
168
+ message: error.message,
169
+ recommendation: "Check ANTHROPIC_API_KEY and batch request format."
170
+ });
171
+ }
172
+ }
173
+ /**
174
+ * batch_check — poll status / retrieve results
175
+ */
176
+ async handleBatchCheck(args) {
177
+ if (!isFeatureEnabled("multiProvider")) {
178
+ return errorResponse({
179
+ errorType: "feature_disabled",
180
+ message: "Multi-provider routing is disabled.",
181
+ recommendation: "Set STACKMEMORY_MULTI_PROVIDER=true to enable multi-provider routing."
182
+ });
183
+ }
184
+ try {
185
+ const batchClient = this.getBatchClient();
186
+ const job = await batchClient.poll(args.batchId);
187
+ if (args.retrieve && job.processing_status === "ended") {
188
+ const results = await batchClient.retrieve(args.batchId);
189
+ return {
190
+ content: [
191
+ {
192
+ type: "text",
193
+ text: JSON.stringify({ job, results }, null, 2)
194
+ }
195
+ ]
196
+ };
197
+ }
198
+ return {
199
+ content: [
200
+ {
201
+ type: "text",
202
+ text: JSON.stringify(
203
+ {
204
+ batchId: args.batchId,
205
+ status: job.processing_status,
206
+ counts: job.request_counts,
207
+ createdAt: job.created_at,
208
+ endedAt: job.ended_at
209
+ },
210
+ null,
211
+ 2
212
+ )
213
+ }
214
+ ]
215
+ };
216
+ } catch (error) {
217
+ return errorResponse({
218
+ errorType: "batch_error",
219
+ message: error.message,
220
+ recommendation: "Check the batchId is valid and ANTHROPIC_API_KEY is set."
221
+ });
222
+ }
223
+ }
224
+ }
225
+ export {
226
+ ProviderHandlers
227
+ };