@soleri/forge 4.2.2 → 5.0.1
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/domain-manager.d.ts +2 -2
- package/dist/domain-manager.js +35 -16
- package/dist/domain-manager.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/knowledge-installer.js +18 -12
- package/dist/knowledge-installer.js.map +1 -1
- package/dist/patching.d.ts +15 -6
- package/dist/patching.js +37 -12
- package/dist/patching.js.map +1 -1
- package/dist/scaffolder.js +18 -28
- package/dist/scaffolder.js.map +1 -1
- package/dist/templates/brain.d.ts +6 -0
- package/dist/templates/brain.js +478 -0
- package/dist/templates/brain.js.map +1 -0
- package/dist/templates/core-facade.js +95 -47
- package/dist/templates/core-facade.js.map +1 -1
- package/dist/templates/entry-point.d.ts +4 -0
- package/dist/templates/entry-point.js +146 -89
- package/dist/templates/entry-point.js.map +1 -1
- package/dist/templates/facade-factory.d.ts +1 -0
- package/dist/templates/facade-factory.js +63 -0
- package/dist/templates/facade-factory.js.map +1 -0
- package/dist/templates/facade-types.d.ts +1 -0
- package/dist/templates/facade-types.js +46 -0
- package/dist/templates/facade-types.js.map +1 -0
- package/dist/templates/intelligence-loader.d.ts +1 -0
- package/dist/templates/intelligence-loader.js +43 -0
- package/dist/templates/intelligence-loader.js.map +1 -0
- package/dist/templates/intelligence-types.d.ts +1 -0
- package/dist/templates/intelligence-types.js +24 -0
- package/dist/templates/intelligence-types.js.map +1 -0
- package/dist/templates/llm-key-pool.d.ts +7 -0
- package/dist/templates/llm-key-pool.js +211 -0
- package/dist/templates/llm-key-pool.js.map +1 -0
- package/dist/templates/llm-types.d.ts +5 -0
- package/dist/templates/llm-types.js +161 -0
- package/dist/templates/llm-types.js.map +1 -0
- package/dist/templates/llm-utils.d.ts +5 -0
- package/dist/templates/llm-utils.js +260 -0
- package/dist/templates/llm-utils.js.map +1 -0
- package/dist/templates/package-json.js +3 -1
- package/dist/templates/package-json.js.map +1 -1
- package/dist/templates/planner.d.ts +5 -0
- package/dist/templates/planner.js +150 -0
- package/dist/templates/planner.js.map +1 -0
- package/dist/templates/test-brain.d.ts +6 -0
- package/dist/templates/test-brain.js +474 -0
- package/dist/templates/test-brain.js.map +1 -0
- package/dist/templates/test-facades.d.ts +1 -1
- package/dist/templates/test-facades.js +182 -456
- package/dist/templates/test-facades.js.map +1 -1
- package/dist/templates/test-llm.d.ts +7 -0
- package/dist/templates/test-llm.js +574 -0
- package/dist/templates/test-llm.js.map +1 -0
- package/dist/templates/test-loader.d.ts +5 -0
- package/dist/templates/test-loader.js +146 -0
- package/dist/templates/test-loader.js.map +1 -0
- package/dist/templates/test-planner.d.ts +5 -0
- package/dist/templates/test-planner.js +271 -0
- package/dist/templates/test-planner.js.map +1 -0
- package/dist/templates/test-vault.d.ts +5 -0
- package/dist/templates/test-vault.js +380 -0
- package/dist/templates/test-vault.js.map +1 -0
- package/dist/templates/vault.d.ts +5 -0
- package/dist/templates/vault.js +263 -0
- package/dist/templates/vault.js.map +1 -0
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/src/__tests__/scaffolder.test.ts +52 -109
- package/src/domain-manager.ts +34 -15
- package/src/knowledge-installer.ts +23 -15
- package/src/patching.ts +44 -12
- package/src/scaffolder.ts +18 -29
- package/src/templates/entry-point.ts +146 -91
- package/src/templates/package-json.ts +3 -1
- package/src/templates/test-facades.ts +182 -458
- package/src/templates/core-facade.ts +0 -517
- package/src/templates/llm-client.ts +0 -301
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
import type { AgentConfig } from '../types.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Generate the LLM client file for a new agent.
|
|
5
|
-
* Contains LLMClient (OpenAI fetch + Anthropic SDK) and ModelRouter (inlined).
|
|
6
|
-
* Uses config.id to resolve ~/.{agentId}/model-routing.json.
|
|
7
|
-
*/
|
|
8
|
-
export function generateLLMClient(config: AgentConfig): string {
|
|
9
|
-
return `/**
|
|
10
|
-
* LLM Client — Unified OpenAI/Anthropic caller with key pool rotation
|
|
11
|
-
* Generated by Soleri — do not edit manually.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import Anthropic from '@anthropic-ai/sdk';
|
|
15
|
-
import { SecretString, LLMError, CircuitBreaker, retry, parseRateLimitHeaders } from '@soleri/core';
|
|
16
|
-
import type {
|
|
17
|
-
LLMCallOptions,
|
|
18
|
-
LLMCallResult,
|
|
19
|
-
RouteEntry,
|
|
20
|
-
RoutingConfig,
|
|
21
|
-
KeyPool,
|
|
22
|
-
} from '@soleri/core';
|
|
23
|
-
import * as fs from 'node:fs';
|
|
24
|
-
import * as path from 'node:path';
|
|
25
|
-
import { homedir } from 'node:os';
|
|
26
|
-
|
|
27
|
-
// =============================================================================
|
|
28
|
-
// CONSTANTS
|
|
29
|
-
// =============================================================================
|
|
30
|
-
|
|
31
|
-
const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
|
|
32
|
-
|
|
33
|
-
// =============================================================================
|
|
34
|
-
// MODEL ROUTER (inlined)
|
|
35
|
-
// =============================================================================
|
|
36
|
-
|
|
37
|
-
function loadRoutingConfig(): RoutingConfig {
|
|
38
|
-
const defaultConfig: RoutingConfig = {
|
|
39
|
-
routes: [],
|
|
40
|
-
defaultOpenAIModel: 'gpt-4o-mini',
|
|
41
|
-
defaultAnthropicModel: 'claude-sonnet-4-20250514',
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const configPath = path.join(
|
|
45
|
-
homedir(),
|
|
46
|
-
'.${config.id}',
|
|
47
|
-
'model-routing.json',
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
if (fs.existsSync(configPath)) {
|
|
52
|
-
const data = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as Partial<RoutingConfig>;
|
|
53
|
-
if (data.routes && Array.isArray(data.routes)) {
|
|
54
|
-
defaultConfig.routes = data.routes;
|
|
55
|
-
}
|
|
56
|
-
if (data.defaultOpenAIModel) {
|
|
57
|
-
defaultConfig.defaultOpenAIModel = data.defaultOpenAIModel;
|
|
58
|
-
}
|
|
59
|
-
if (data.defaultAnthropicModel) {
|
|
60
|
-
defaultConfig.defaultAnthropicModel = data.defaultAnthropicModel;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
} catch {
|
|
64
|
-
// Config not available — use defaults
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return defaultConfig;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function inferProvider(model: string): 'openai' | 'anthropic' {
|
|
71
|
-
if (model.startsWith('claude-') || model.startsWith('anthropic/')) {
|
|
72
|
-
return 'anthropic';
|
|
73
|
-
}
|
|
74
|
-
return 'openai';
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
class ModelRouter {
|
|
78
|
-
private config: RoutingConfig;
|
|
79
|
-
|
|
80
|
-
constructor(config?: RoutingConfig) {
|
|
81
|
-
this.config = config || loadRoutingConfig();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
resolve(
|
|
85
|
-
caller: string,
|
|
86
|
-
task?: string,
|
|
87
|
-
originalModel?: string,
|
|
88
|
-
): { model: string; provider: 'openai' | 'anthropic' } {
|
|
89
|
-
if (task) {
|
|
90
|
-
const exactMatch = this.config.routes.find(
|
|
91
|
-
(r) => r.caller === caller && r.task === task,
|
|
92
|
-
);
|
|
93
|
-
if (exactMatch) {
|
|
94
|
-
return { model: exactMatch.model, provider: exactMatch.provider };
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const callerMatch = this.config.routes.find(
|
|
99
|
-
(r) => r.caller === caller && !r.task,
|
|
100
|
-
);
|
|
101
|
-
if (callerMatch) {
|
|
102
|
-
return { model: callerMatch.model, provider: callerMatch.provider };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (originalModel) {
|
|
106
|
-
const provider = inferProvider(originalModel);
|
|
107
|
-
return { model: originalModel, provider };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return { model: this.config.defaultOpenAIModel, provider: 'openai' };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
getRoutes(): RouteEntry[] {
|
|
114
|
-
return [...this.config.routes];
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// =============================================================================
|
|
119
|
-
// LLM CLIENT
|
|
120
|
-
// =============================================================================
|
|
121
|
-
|
|
122
|
-
export class LLMClient {
|
|
123
|
-
private openaiKeyPool: KeyPool;
|
|
124
|
-
private anthropicKeyPool: KeyPool;
|
|
125
|
-
private anthropicClient: Anthropic | null = null;
|
|
126
|
-
private anthropicBreaker: CircuitBreaker;
|
|
127
|
-
private anthropicKeyFingerprint: string = '';
|
|
128
|
-
private router: ModelRouter;
|
|
129
|
-
|
|
130
|
-
constructor(openaiKeyPool: KeyPool, anthropicKeyPool: KeyPool) {
|
|
131
|
-
this.openaiKeyPool = openaiKeyPool;
|
|
132
|
-
this.anthropicKeyPool = anthropicKeyPool;
|
|
133
|
-
this.anthropicBreaker = new CircuitBreaker({
|
|
134
|
-
name: 'llm-anthropic',
|
|
135
|
-
failureThreshold: 5,
|
|
136
|
-
resetTimeoutMs: 60_000,
|
|
137
|
-
});
|
|
138
|
-
this.router = new ModelRouter();
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async complete(options: LLMCallOptions): Promise<LLMCallResult> {
|
|
142
|
-
const routed = this.router.resolve(
|
|
143
|
-
options.caller,
|
|
144
|
-
options.task,
|
|
145
|
-
options.model,
|
|
146
|
-
);
|
|
147
|
-
const resolvedOptions = { ...options, model: routed.model, provider: routed.provider };
|
|
148
|
-
|
|
149
|
-
return resolvedOptions.provider === 'anthropic'
|
|
150
|
-
? this.callAnthropic(resolvedOptions)
|
|
151
|
-
: this.callOpenAI(resolvedOptions);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
isAvailable(): { openai: boolean; anthropic: boolean } {
|
|
155
|
-
return {
|
|
156
|
-
openai: this.openaiKeyPool.hasKeys,
|
|
157
|
-
anthropic: this.anthropicKeyPool.hasKeys,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
getRoutes(): RouteEntry[] {
|
|
162
|
-
return this.router.getRoutes();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ===========================================================================
|
|
166
|
-
// OPENAI
|
|
167
|
-
// ===========================================================================
|
|
168
|
-
|
|
169
|
-
private async callOpenAI(options: LLMCallOptions): Promise<LLMCallResult> {
|
|
170
|
-
const keyPool = this.openaiKeyPool.hasKeys ? this.openaiKeyPool : null;
|
|
171
|
-
|
|
172
|
-
if (!keyPool) {
|
|
173
|
-
throw new LLMError('OpenAI API key not configured', { retryable: false });
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const start = Date.now();
|
|
177
|
-
|
|
178
|
-
const doRequest = async (): Promise<LLMCallResult> => {
|
|
179
|
-
const apiKey = keyPool.getActiveKey().expose();
|
|
180
|
-
const keyIndex = keyPool.activeKeyIndex;
|
|
181
|
-
|
|
182
|
-
const response = await fetch(OPENAI_API_URL, {
|
|
183
|
-
method: 'POST',
|
|
184
|
-
headers: {
|
|
185
|
-
'Content-Type': 'application/json',
|
|
186
|
-
Authorization: \`Bearer \${apiKey}\`,
|
|
187
|
-
},
|
|
188
|
-
body: JSON.stringify({
|
|
189
|
-
model: options.model,
|
|
190
|
-
messages: [
|
|
191
|
-
{ role: 'system', content: options.systemPrompt },
|
|
192
|
-
{ role: 'user', content: options.userPrompt },
|
|
193
|
-
],
|
|
194
|
-
temperature: options.temperature ?? 0.3,
|
|
195
|
-
max_completion_tokens: options.maxTokens ?? 500,
|
|
196
|
-
}),
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
if (response.headers) {
|
|
200
|
-
const rateLimits = parseRateLimitHeaders(response.headers);
|
|
201
|
-
if (rateLimits.remaining !== null) {
|
|
202
|
-
keyPool.updateQuota(keyIndex, rateLimits.remaining);
|
|
203
|
-
keyPool.rotatePreemptive();
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (!response.ok) {
|
|
208
|
-
if (response.status === 429 && keyPool.poolSize > 1) {
|
|
209
|
-
keyPool.rotateOnError();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const errorBody = await response.text();
|
|
213
|
-
throw new LLMError(
|
|
214
|
-
\`OpenAI API error: \${response.status} - \${errorBody}\`,
|
|
215
|
-
{ retryable: response.status === 429 || response.status >= 500, statusCode: response.status },
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const data = (await response.json()) as {
|
|
220
|
-
choices: Array<{ message: { content: string } }>;
|
|
221
|
-
usage?: { prompt_tokens?: number; completion_tokens?: number };
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
text: data.choices[0]?.message?.content || '',
|
|
226
|
-
model: options.model,
|
|
227
|
-
provider: 'openai' as const,
|
|
228
|
-
inputTokens: data.usage?.prompt_tokens,
|
|
229
|
-
outputTokens: data.usage?.completion_tokens,
|
|
230
|
-
durationMs: Date.now() - start,
|
|
231
|
-
};
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
return retry(doRequest, { maxAttempts: 3 });
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// ===========================================================================
|
|
238
|
-
// ANTHROPIC
|
|
239
|
-
// ===========================================================================
|
|
240
|
-
|
|
241
|
-
private async callAnthropic(options: LLMCallOptions): Promise<LLMCallResult> {
|
|
242
|
-
const client = this.getAnthropicClient();
|
|
243
|
-
if (!client) {
|
|
244
|
-
throw new LLMError('Anthropic API key not configured', { retryable: false });
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const start = Date.now();
|
|
248
|
-
|
|
249
|
-
return this.anthropicBreaker.call(() =>
|
|
250
|
-
retry(
|
|
251
|
-
async () => {
|
|
252
|
-
const response = await client.messages.create(
|
|
253
|
-
{
|
|
254
|
-
model: options.model,
|
|
255
|
-
max_tokens: options.maxTokens ?? 1024,
|
|
256
|
-
system: options.systemPrompt,
|
|
257
|
-
messages: [{ role: 'user', content: options.userPrompt }],
|
|
258
|
-
},
|
|
259
|
-
{ timeout: 60_000 },
|
|
260
|
-
);
|
|
261
|
-
|
|
262
|
-
const text = response.content
|
|
263
|
-
.filter(
|
|
264
|
-
(block): block is Anthropic.TextBlock => block.type === 'text',
|
|
265
|
-
)
|
|
266
|
-
.map((block) => block.text)
|
|
267
|
-
.join('\\n');
|
|
268
|
-
|
|
269
|
-
return {
|
|
270
|
-
text,
|
|
271
|
-
model: options.model,
|
|
272
|
-
provider: 'anthropic' as const,
|
|
273
|
-
inputTokens: response.usage?.input_tokens,
|
|
274
|
-
outputTokens: response.usage?.output_tokens,
|
|
275
|
-
durationMs: Date.now() - start,
|
|
276
|
-
};
|
|
277
|
-
},
|
|
278
|
-
{ maxAttempts: 2 },
|
|
279
|
-
),
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
private getAnthropicClient(): Anthropic | null {
|
|
284
|
-
if (!this.anthropicKeyPool.hasKeys) return null;
|
|
285
|
-
|
|
286
|
-
const currentKey = this.anthropicKeyPool.getActiveKey().expose();
|
|
287
|
-
const currentFingerprint = currentKey.slice(-8);
|
|
288
|
-
|
|
289
|
-
if (currentFingerprint !== this.anthropicKeyFingerprint) {
|
|
290
|
-
this.anthropicClient = null;
|
|
291
|
-
this.anthropicKeyFingerprint = currentFingerprint;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (this.anthropicClient) return this.anthropicClient;
|
|
295
|
-
|
|
296
|
-
this.anthropicClient = new Anthropic({ apiKey: currentKey });
|
|
297
|
-
return this.anthropicClient;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
`;
|
|
301
|
-
}
|