@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.
- package/README.md +66 -270
- package/dist/src/cli/claude-sm.js +9 -0
- package/dist/src/cli/codex-sm.js +9 -0
- package/dist/src/cli/commands/audit.js +134 -0
- package/dist/src/cli/commands/bench.js +252 -0
- package/dist/src/cli/commands/dashboard.js +2 -1
- package/dist/src/cli/commands/stats.js +118 -0
- package/dist/src/cli/index.js +6 -0
- package/dist/src/core/config/feature-flags.js +7 -1
- package/dist/src/core/context/enhanced-rehydration.js +24 -5
- package/dist/src/core/extensions/cerebras-adapter.js +28 -0
- package/dist/src/core/extensions/deepinfra-adapter.js +32 -0
- package/dist/src/core/extensions/provider-adapter.js +33 -240
- package/dist/src/core/models/complexity-scorer.js +154 -0
- package/dist/src/core/models/model-router.js +230 -36
- package/dist/src/core/models/provider-pricing.js +63 -0
- package/dist/src/core/models/sensitive-guard.js +112 -0
- package/dist/src/core/monitoring/feedback-loops.js +88 -0
- package/dist/src/features/sweep/pty-wrapper.js +9 -0
- package/dist/src/hooks/daemon.js +8 -0
- package/dist/src/hooks/graphiti-hooks.js +104 -0
- package/dist/src/hooks/schemas.js +12 -1
- package/dist/src/integrations/anthropic/batch-client.js +256 -0
- package/dist/src/integrations/anthropic/client.js +87 -72
- package/dist/src/integrations/claude-code/subagent-client.js +133 -12
- package/dist/src/integrations/graphiti/client.js +115 -0
- package/dist/src/integrations/graphiti/config.js +17 -0
- package/dist/src/integrations/graphiti/types.js +4 -0
- package/dist/src/integrations/mcp/handlers/index.js +25 -1
- package/dist/src/integrations/mcp/handlers/provider-handlers.js +227 -0
- package/dist/src/integrations/mcp/server.js +207 -1
- package/dist/src/integrations/mcp/tool-definitions.js +38 -1
- package/dist/src/orchestrators/multimodal/baselines.js +128 -0
- package/dist/src/orchestrators/multimodal/constants.js +9 -1
- package/dist/src/orchestrators/multimodal/harness.js +86 -6
- package/dist/src/orchestrators/multimodal/providers.js +113 -2
- package/dist/src/skills/recursive-agent-orchestrator.js +15 -10
- package/dist/src/utils/fuzzy-edit.js +162 -0
- 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
|
|
29
|
-
*
|
|
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(
|
|
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
|
|
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}`, {
|
|
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(
|
|
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(
|
|
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
|
+
};
|
|
@@ -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
|
+
};
|