@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.
- 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/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 +16 -4
- 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
|
@@ -0,0 +1,256 @@
|
|
|
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 { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import { logger } from "../../core/monitoring/logger.js";
|
|
9
|
+
const BATCH_JOBS_PATH = join(homedir(), ".stackmemory", "batch-jobs.json");
|
|
10
|
+
const DEFAULT_POLL_INTERVAL_MS = 3e4;
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 2 * 60 * 60 * 1e3;
|
|
12
|
+
class AnthropicBatchClient {
|
|
13
|
+
apiKey;
|
|
14
|
+
baseUrl;
|
|
15
|
+
mockMode;
|
|
16
|
+
constructor(config) {
|
|
17
|
+
this.apiKey = config?.apiKey !== void 0 ? config.apiKey : process.env["ANTHROPIC_API_KEY"] || "";
|
|
18
|
+
this.baseUrl = config?.baseUrl || "https://api.anthropic.com";
|
|
19
|
+
this.mockMode = config?.mockMode ?? !this.apiKey;
|
|
20
|
+
if (this.mockMode) {
|
|
21
|
+
logger.warn("AnthropicBatchClient: no API key, using mock mode");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Submit a batch of requests
|
|
26
|
+
*/
|
|
27
|
+
async submit(requests, description) {
|
|
28
|
+
if (this.mockMode) {
|
|
29
|
+
const batchId = `batch_mock_${Date.now()}`;
|
|
30
|
+
this.persistJob({
|
|
31
|
+
batchId,
|
|
32
|
+
status: "ended",
|
|
33
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
34
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
35
|
+
requestCount: requests.length,
|
|
36
|
+
description
|
|
37
|
+
});
|
|
38
|
+
return batchId;
|
|
39
|
+
}
|
|
40
|
+
const response = await fetch(`${this.baseUrl}/v1/messages/batches`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
"x-api-key": this.apiKey,
|
|
45
|
+
"anthropic-version": "2023-06-01"
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify({ requests })
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const errText = await response.text();
|
|
51
|
+
throw new Error(`Batch submit failed: ${response.status} ${errText}`);
|
|
52
|
+
}
|
|
53
|
+
const job = await response.json();
|
|
54
|
+
this.persistJob({
|
|
55
|
+
batchId: job.id,
|
|
56
|
+
status: job.processing_status,
|
|
57
|
+
createdAt: job.created_at,
|
|
58
|
+
requestCount: requests.length,
|
|
59
|
+
description
|
|
60
|
+
});
|
|
61
|
+
logger.info("Batch submitted", { batchId: job.id, count: requests.length });
|
|
62
|
+
return job.id;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Poll batch job status
|
|
66
|
+
*/
|
|
67
|
+
async poll(batchId) {
|
|
68
|
+
if (this.mockMode) {
|
|
69
|
+
return this.mockBatchJob(batchId);
|
|
70
|
+
}
|
|
71
|
+
const response = await fetch(
|
|
72
|
+
`${this.baseUrl}/v1/messages/batches/${batchId}`,
|
|
73
|
+
{
|
|
74
|
+
headers: {
|
|
75
|
+
"x-api-key": this.apiKey,
|
|
76
|
+
"anthropic-version": "2023-06-01"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
const errText = await response.text();
|
|
82
|
+
throw new Error(`Batch poll failed: ${response.status} ${errText}`);
|
|
83
|
+
}
|
|
84
|
+
const job = await response.json();
|
|
85
|
+
this.updateJobStatus(batchId, job.processing_status, job.ended_at);
|
|
86
|
+
return job;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Retrieve batch results
|
|
90
|
+
*/
|
|
91
|
+
async retrieve(batchId) {
|
|
92
|
+
if (this.mockMode) {
|
|
93
|
+
return this.mockBatchResults(batchId);
|
|
94
|
+
}
|
|
95
|
+
const job = await this.poll(batchId);
|
|
96
|
+
if (job.processing_status !== "ended") {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Batch ${batchId} not finished: ${job.processing_status}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (!job.results_url) {
|
|
102
|
+
throw new Error(`Batch ${batchId} has no results URL`);
|
|
103
|
+
}
|
|
104
|
+
const response = await fetch(job.results_url, {
|
|
105
|
+
headers: {
|
|
106
|
+
"x-api-key": this.apiKey,
|
|
107
|
+
"anthropic-version": "2023-06-01"
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
throw new Error(`Batch retrieve failed: ${response.status}`);
|
|
112
|
+
}
|
|
113
|
+
const text = await response.text();
|
|
114
|
+
return text.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Poll until batch completes or times out
|
|
118
|
+
*/
|
|
119
|
+
async waitForCompletion(batchId, timeoutMs = DEFAULT_TIMEOUT_MS, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS) {
|
|
120
|
+
const deadline = Date.now() + timeoutMs;
|
|
121
|
+
while (Date.now() < deadline) {
|
|
122
|
+
const job = await this.poll(batchId);
|
|
123
|
+
if (job.processing_status === "ended") {
|
|
124
|
+
return job;
|
|
125
|
+
}
|
|
126
|
+
const remaining = deadline - Date.now();
|
|
127
|
+
const waitTime = Math.min(pollIntervalMs, remaining);
|
|
128
|
+
if (waitTime <= 0) break;
|
|
129
|
+
await new Promise((resolve) => {
|
|
130
|
+
const timer = setTimeout(() => resolve(), waitTime);
|
|
131
|
+
if (typeof timer === "object" && "unref" in timer) {
|
|
132
|
+
timer.unref();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`Batch ${batchId} timed out after ${timeoutMs}ms`);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Submit and wait for results (convenience)
|
|
140
|
+
*/
|
|
141
|
+
async submitAndWait(requests, timeoutMs = DEFAULT_TIMEOUT_MS, description) {
|
|
142
|
+
const batchId = await this.submit(requests, description);
|
|
143
|
+
await this.waitForCompletion(batchId, timeoutMs);
|
|
144
|
+
return this.retrieve(batchId);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Cancel a batch job
|
|
148
|
+
*/
|
|
149
|
+
async cancel(batchId) {
|
|
150
|
+
if (this.mockMode) {
|
|
151
|
+
return this.mockBatchJob(batchId, "canceling");
|
|
152
|
+
}
|
|
153
|
+
const response = await fetch(
|
|
154
|
+
`${this.baseUrl}/v1/messages/batches/${batchId}/cancel`,
|
|
155
|
+
{
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: {
|
|
158
|
+
"x-api-key": this.apiKey,
|
|
159
|
+
"anthropic-version": "2023-06-01"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
throw new Error(`Batch cancel failed: ${response.status}`);
|
|
165
|
+
}
|
|
166
|
+
const job = await response.json();
|
|
167
|
+
this.updateJobStatus(batchId, job.processing_status);
|
|
168
|
+
return job;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* List stored batch jobs
|
|
172
|
+
*/
|
|
173
|
+
listJobs() {
|
|
174
|
+
return this.loadStoredJobs();
|
|
175
|
+
}
|
|
176
|
+
// ── Persistence ───────────────────────────────────────────────────────
|
|
177
|
+
persistJob(job) {
|
|
178
|
+
const jobs = this.loadStoredJobs();
|
|
179
|
+
const existing = jobs.findIndex((j) => j.batchId === job.batchId);
|
|
180
|
+
if (existing >= 0) {
|
|
181
|
+
jobs[existing] = job;
|
|
182
|
+
} else {
|
|
183
|
+
jobs.push(job);
|
|
184
|
+
}
|
|
185
|
+
const trimmed = jobs.slice(-50);
|
|
186
|
+
this.saveStoredJobs(trimmed);
|
|
187
|
+
}
|
|
188
|
+
updateJobStatus(batchId, status, endedAt) {
|
|
189
|
+
const jobs = this.loadStoredJobs();
|
|
190
|
+
const job = jobs.find((j) => j.batchId === batchId);
|
|
191
|
+
if (job) {
|
|
192
|
+
job.status = status;
|
|
193
|
+
if (endedAt) job.endedAt = endedAt;
|
|
194
|
+
this.saveStoredJobs(jobs);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
loadStoredJobs() {
|
|
198
|
+
try {
|
|
199
|
+
if (existsSync(BATCH_JOBS_PATH)) {
|
|
200
|
+
return JSON.parse(readFileSync(BATCH_JOBS_PATH, "utf8"));
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
saveStoredJobs(jobs) {
|
|
207
|
+
try {
|
|
208
|
+
const dir = join(homedir(), ".stackmemory");
|
|
209
|
+
if (!existsSync(dir)) {
|
|
210
|
+
mkdirSync(dir, { recursive: true });
|
|
211
|
+
}
|
|
212
|
+
writeFileSync(BATCH_JOBS_PATH, JSON.stringify(jobs, null, 2));
|
|
213
|
+
} catch {
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// ── Mock mode ─────────────────────────────────────────────────────────
|
|
217
|
+
mockBatchJob(batchId, status = "ended") {
|
|
218
|
+
return {
|
|
219
|
+
id: batchId,
|
|
220
|
+
type: "message_batch",
|
|
221
|
+
processing_status: status,
|
|
222
|
+
request_counts: {
|
|
223
|
+
processing: 0,
|
|
224
|
+
succeeded: 1,
|
|
225
|
+
errored: 0,
|
|
226
|
+
canceled: 0,
|
|
227
|
+
expired: 0
|
|
228
|
+
},
|
|
229
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
230
|
+
ended_at: status === "ended" ? (/* @__PURE__ */ new Date()).toISOString() : void 0
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
mockBatchResults(batchId) {
|
|
234
|
+
const jobs = this.loadStoredJobs();
|
|
235
|
+
const job = jobs.find((j) => j.batchId === batchId);
|
|
236
|
+
const count = job?.requestCount || 1;
|
|
237
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
238
|
+
custom_id: `req_${i}`,
|
|
239
|
+
result: {
|
|
240
|
+
type: "succeeded",
|
|
241
|
+
message: {
|
|
242
|
+
id: `msg_mock_${i}`,
|
|
243
|
+
content: [
|
|
244
|
+
{ type: "text", text: `Mock batch response for request ${i}` }
|
|
245
|
+
],
|
|
246
|
+
model: "claude-sonnet-4-5-20250929",
|
|
247
|
+
stop_reason: "end_turn",
|
|
248
|
+
usage: { input_tokens: 100, output_tokens: 50 }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
export {
|
|
255
|
+
AnthropicBatchClient
|
|
256
|
+
};
|
|
@@ -3,6 +3,7 @@ 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
|
class AnthropicClient {
|
|
7
8
|
apiKey;
|
|
8
9
|
baseURL;
|
|
@@ -31,17 +32,21 @@ class AnthropicClient {
|
|
|
31
32
|
* Send completion request to Claude
|
|
32
33
|
*/
|
|
33
34
|
async complete(request) {
|
|
35
|
+
const enhancedRequest = {
|
|
36
|
+
...request,
|
|
37
|
+
systemPrompt: request.systemPrompt + STRUCTURED_RESPONSE_SUFFIX
|
|
38
|
+
};
|
|
34
39
|
await this.checkRateLimit();
|
|
35
40
|
logger.debug("Sending completion request", {
|
|
36
|
-
model:
|
|
37
|
-
promptLength:
|
|
38
|
-
maxTokens:
|
|
41
|
+
model: enhancedRequest.model,
|
|
42
|
+
promptLength: enhancedRequest.prompt.length,
|
|
43
|
+
maxTokens: enhancedRequest.maxTokens
|
|
39
44
|
});
|
|
40
45
|
if (!this.apiKey) {
|
|
41
|
-
return this.mockComplete(
|
|
46
|
+
return this.mockComplete(enhancedRequest);
|
|
42
47
|
}
|
|
43
48
|
try {
|
|
44
|
-
const response = await this.sendRequest(
|
|
49
|
+
const response = await this.sendRequest(enhancedRequest);
|
|
45
50
|
return response.content;
|
|
46
51
|
} catch (error) {
|
|
47
52
|
logger.error("Anthropic API error", { error });
|
|
@@ -57,7 +62,9 @@ class AnthropicClient {
|
|
|
57
62
|
} catch (error) {
|
|
58
63
|
if (attempt < this.maxRetries) {
|
|
59
64
|
const delay = Math.pow(2, attempt) * 1e3;
|
|
60
|
-
logger.warn(
|
|
65
|
+
logger.warn(
|
|
66
|
+
`Retrying after ${delay}ms (attempt ${attempt}/${this.maxRetries})`
|
|
67
|
+
);
|
|
61
68
|
await this.delay(delay);
|
|
62
69
|
return this.sendRequest(request, attempt + 1);
|
|
63
70
|
}
|
|
@@ -85,44 +92,48 @@ class AnthropicClient {
|
|
|
85
92
|
* Mock response generators
|
|
86
93
|
*/
|
|
87
94
|
mockPlanningResponse(prompt) {
|
|
88
|
-
return JSON.stringify(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
95
|
+
return JSON.stringify(
|
|
96
|
+
{
|
|
97
|
+
plan: {
|
|
98
|
+
type: "sequential",
|
|
99
|
+
tasks: [
|
|
100
|
+
{
|
|
101
|
+
id: "task-1",
|
|
102
|
+
description: "Analyze requirements",
|
|
103
|
+
agent: "context",
|
|
104
|
+
dependencies: []
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "task-2",
|
|
108
|
+
type: "parallel",
|
|
109
|
+
description: "Implementation phase",
|
|
110
|
+
children: [
|
|
111
|
+
{
|
|
112
|
+
id: "task-2a",
|
|
113
|
+
description: "Write core logic",
|
|
114
|
+
agent: "code",
|
|
115
|
+
dependencies: ["task-1"]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "task-2b",
|
|
119
|
+
description: "Write tests",
|
|
120
|
+
agent: "testing",
|
|
121
|
+
dependencies: ["task-1"]
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "task-3",
|
|
127
|
+
description: "Review and improve",
|
|
128
|
+
agent: "review",
|
|
129
|
+
dependencies: ["task-2"]
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
null,
|
|
135
|
+
2
|
|
136
|
+
);
|
|
126
137
|
}
|
|
127
138
|
mockCodeResponse(prompt) {
|
|
128
139
|
return `
|
|
@@ -166,34 +177,38 @@ describe('validateInput', () => {
|
|
|
166
177
|
`.trim();
|
|
167
178
|
}
|
|
168
179
|
mockReviewResponse(prompt) {
|
|
169
|
-
return JSON.stringify(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
180
|
+
return JSON.stringify(
|
|
181
|
+
{
|
|
182
|
+
quality: 0.75,
|
|
183
|
+
issues: [
|
|
184
|
+
"Missing error handling in processTask function",
|
|
185
|
+
"No input validation before processing",
|
|
186
|
+
"Tests could cover more edge cases"
|
|
187
|
+
],
|
|
188
|
+
suggestions: [
|
|
189
|
+
"Add try-catch block in processTask",
|
|
190
|
+
"Validate input length and type",
|
|
191
|
+
"Add tests for special characters and long inputs",
|
|
192
|
+
"Consider adding performance tests"
|
|
193
|
+
],
|
|
194
|
+
improvements: [
|
|
195
|
+
{
|
|
196
|
+
file: "implementation.ts",
|
|
197
|
+
line: 3,
|
|
198
|
+
suggestion: "Add input validation",
|
|
199
|
+
priority: "high"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
file: "tests.ts",
|
|
203
|
+
line: 15,
|
|
204
|
+
suggestion: "Add edge case tests",
|
|
205
|
+
priority: "medium"
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
},
|
|
209
|
+
null,
|
|
210
|
+
2
|
|
211
|
+
);
|
|
197
212
|
}
|
|
198
213
|
/**
|
|
199
214
|
* Create mock response object
|
|
@@ -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: [
|