@kirosnn/mosaic 0.71.0 → 0.73.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 +1 -5
- package/package.json +4 -2
- package/src/agent/Agent.ts +353 -131
- package/src/agent/context.ts +4 -4
- package/src/agent/prompts/systemPrompt.ts +15 -6
- package/src/agent/prompts/toolsPrompt.ts +75 -10
- package/src/agent/provider/anthropic.ts +100 -100
- package/src/agent/provider/google.ts +102 -102
- package/src/agent/provider/mistral.ts +95 -95
- package/src/agent/provider/ollama.ts +77 -60
- package/src/agent/provider/openai.ts +42 -38
- package/src/agent/provider/rateLimit.ts +178 -0
- package/src/agent/provider/xai.ts +99 -99
- package/src/agent/tools/definitions.ts +19 -9
- package/src/agent/tools/executor.ts +95 -85
- package/src/agent/tools/exploreExecutor.ts +8 -10
- package/src/agent/tools/grep.ts +30 -29
- package/src/agent/tools/question.ts +7 -1
- package/src/agent/types.ts +9 -8
- package/src/components/App.tsx +45 -45
- package/src/components/CustomInput.tsx +214 -36
- package/src/components/Main.tsx +1146 -954
- package/src/components/Setup.tsx +1 -1
- package/src/components/Welcome.tsx +1 -1
- package/src/components/main/ApprovalPanel.tsx +4 -3
- package/src/components/main/ChatPage.tsx +858 -675
- package/src/components/main/HomePage.tsx +53 -38
- package/src/components/main/QuestionPanel.tsx +52 -7
- package/src/components/main/ThinkingIndicator.tsx +2 -1
- package/src/index.tsx +50 -20
- package/src/mcp/approvalPolicy.ts +148 -0
- package/src/mcp/cli/add.ts +185 -0
- package/src/mcp/cli/doctor.ts +77 -0
- package/src/mcp/cli/index.ts +85 -0
- package/src/mcp/cli/list.ts +50 -0
- package/src/mcp/cli/logs.ts +24 -0
- package/src/mcp/cli/manage.ts +99 -0
- package/src/mcp/cli/show.ts +53 -0
- package/src/mcp/cli/tools.ts +77 -0
- package/src/mcp/config.ts +223 -0
- package/src/mcp/index.ts +80 -0
- package/src/mcp/processManager.ts +299 -0
- package/src/mcp/rateLimiter.ts +50 -0
- package/src/mcp/registry.ts +151 -0
- package/src/mcp/schemaConverter.ts +100 -0
- package/src/mcp/servers/navigation.ts +854 -0
- package/src/mcp/toolCatalog.ts +169 -0
- package/src/mcp/types.ts +95 -0
- package/src/utils/approvalBridge.ts +17 -5
- package/src/utils/commands/compact.ts +30 -0
- package/src/utils/commands/echo.ts +1 -1
- package/src/utils/commands/index.ts +4 -6
- package/src/utils/commands/new.ts +15 -0
- package/src/utils/commands/types.ts +3 -0
- package/src/utils/config.ts +3 -1
- package/src/utils/diffRendering.tsx +1 -3
- package/src/utils/exploreBridge.ts +10 -0
- package/src/utils/markdown.tsx +163 -99
- package/src/utils/models.ts +31 -9
- package/src/utils/questionBridge.ts +36 -1
- package/src/utils/tokenEstimator.ts +32 -0
- package/src/utils/toolFormatting.ts +268 -7
- package/src/web/app.tsx +72 -72
- package/src/web/components/HomePage.tsx +7 -7
- package/src/web/components/MessageItem.tsx +22 -22
- package/src/web/components/QuestionPanel.tsx +72 -12
- package/src/web/components/Sidebar.tsx +0 -2
- package/src/web/components/ThinkingIndicator.tsx +1 -0
- package/src/web/server.tsx +767 -683
- package/src/utils/commands/redo.ts +0 -74
- package/src/utils/commands/sessions.ts +0 -129
- package/src/utils/commands/undo.ts +0 -75
- package/src/utils/undoRedo.ts +0 -429
- package/src/utils/undoRedoBridge.ts +0 -45
- package/src/utils/undoRedoDb.ts +0 -338
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { streamText, CoreMessage } from 'ai';
|
|
2
2
|
import { createMistral } from '@ai-sdk/mistral';
|
|
3
|
-
import { AgentEvent, Provider, ProviderConfig, ProviderSendOptions } from '../types';
|
|
3
|
+
import { AgentEvent, Provider, ProviderConfig, ProviderSendOptions } from '../types';
|
|
4
|
+
import { getRetryDecision, normalizeError, runWithRetry } from './rateLimit';
|
|
4
5
|
|
|
5
6
|
export class MistralProvider implements Provider {
|
|
6
7
|
async *sendMessage(
|
|
@@ -15,103 +16,102 @@ export class MistralProvider implements Provider {
|
|
|
15
16
|
apiKey: cleanApiKey,
|
|
16
17
|
});
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
{
|
|
94
|
-
const err = c.error;
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (options?.abortSignal?.aborted) return;
|
|
19
|
+
try {
|
|
20
|
+
let stepCounter = 0;
|
|
21
|
+
|
|
22
|
+
yield* runWithRetry(async function* () {
|
|
23
|
+
const result = streamText({
|
|
24
|
+
model: mistral(cleanModel),
|
|
25
|
+
messages: messages,
|
|
26
|
+
system: config.systemPrompt,
|
|
27
|
+
tools: config.tools,
|
|
28
|
+
maxSteps: config.maxSteps || 100,
|
|
29
|
+
abortSignal: options?.abortSignal
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
for await (const chunk of result.fullStream as any) {
|
|
33
|
+
const c: any = chunk;
|
|
34
|
+
switch (c.type) {
|
|
35
|
+
case 'reasoning':
|
|
36
|
+
if (c.textDelta) {
|
|
37
|
+
yield {
|
|
38
|
+
type: 'reasoning-delta',
|
|
39
|
+
content: c.textDelta,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
|
|
44
|
+
case 'text-delta':
|
|
45
|
+
yield {
|
|
46
|
+
type: 'text-delta',
|
|
47
|
+
content: c.textDelta,
|
|
48
|
+
};
|
|
49
|
+
break;
|
|
50
|
+
|
|
51
|
+
case 'step-start':
|
|
52
|
+
yield {
|
|
53
|
+
type: 'step-start',
|
|
54
|
+
stepNumber: typeof c.stepIndex === 'number' ? c.stepIndex : stepCounter,
|
|
55
|
+
};
|
|
56
|
+
stepCounter++;
|
|
57
|
+
break;
|
|
58
|
+
|
|
59
|
+
case 'step-finish':
|
|
60
|
+
yield {
|
|
61
|
+
type: 'step-finish',
|
|
62
|
+
stepNumber:
|
|
63
|
+
typeof c.stepIndex === 'number' ? c.stepIndex : Math.max(0, stepCounter - 1),
|
|
64
|
+
finishReason: String(c.finishReason ?? 'stop'),
|
|
65
|
+
};
|
|
66
|
+
break;
|
|
67
|
+
|
|
68
|
+
case 'tool-call':
|
|
69
|
+
yield {
|
|
70
|
+
type: 'tool-call-end',
|
|
71
|
+
toolCallId: String(c.toolCallId ?? ''),
|
|
72
|
+
toolName: String(c.toolName ?? ''),
|
|
73
|
+
args: (c.args ?? {}) as Record<string, unknown>,
|
|
74
|
+
};
|
|
75
|
+
break;
|
|
76
|
+
|
|
77
|
+
case 'tool-result':
|
|
78
|
+
yield {
|
|
79
|
+
type: 'tool-result',
|
|
80
|
+
toolCallId: String(c.toolCallId ?? ''),
|
|
81
|
+
toolName: String(c.toolName ?? ''),
|
|
82
|
+
result: c.result,
|
|
83
|
+
};
|
|
84
|
+
break;
|
|
85
|
+
|
|
86
|
+
case 'finish':
|
|
87
|
+
yield {
|
|
88
|
+
type: 'finish',
|
|
89
|
+
finishReason: String(c.finishReason ?? 'stop'),
|
|
90
|
+
usage: c.usage,
|
|
91
|
+
};
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
case 'error': {
|
|
95
|
+
const err = normalizeError(c.error);
|
|
96
|
+
const decision = getRetryDecision(err);
|
|
97
|
+
if (decision.shouldRetry) {
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
yield {
|
|
101
|
+
type: 'error',
|
|
102
|
+
error: err.message,
|
|
103
|
+
};
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}, { abortSignal: options?.abortSignal });
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (options?.abortSignal?.aborted) return;
|
|
111
111
|
yield {
|
|
112
112
|
type: 'error',
|
|
113
113
|
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
-
}
|
|
117
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Ollama } from 'ollama';
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
|
-
import { CoreMessage, CoreTool } from 'ai';
|
|
4
|
-
import { AgentEvent, Provider, ProviderConfig, ProviderSendOptions } from '../types';
|
|
5
|
-
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
6
|
-
import { z } from 'zod';
|
|
7
|
-
import { shouldEnableReasoning } from './reasoning';
|
|
3
|
+
import { CoreMessage, CoreTool } from 'ai';
|
|
4
|
+
import { AgentEvent, Provider, ProviderConfig, ProviderSendOptions } from '../types';
|
|
5
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { shouldEnableReasoning } from './reasoning';
|
|
8
|
+
import { getErrorSignature, getRetryDecision } from './rateLimit';
|
|
8
9
|
|
|
9
10
|
let serveStartPromise: Promise<void> | null = null;
|
|
10
11
|
const pullPromises = new Map<string, Promise<void>>();
|
|
@@ -12,6 +13,8 @@ const pullPromises = new Map<string, Promise<void>>();
|
|
|
12
13
|
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
13
14
|
|
|
14
15
|
function isTransientError(error: unknown): boolean {
|
|
16
|
+
const decision = getRetryDecision(error);
|
|
17
|
+
if (decision.shouldRetry) return true;
|
|
15
18
|
const msg = error instanceof Error ? error.message : String(error);
|
|
16
19
|
return (
|
|
17
20
|
msg.includes('ECONNREFUSED') ||
|
|
@@ -24,13 +27,27 @@ function isTransientError(error: unknown): boolean {
|
|
|
24
27
|
|
|
25
28
|
async function retry<T>(fn: () => Promise<T>, retries: number, baseDelayMs: number): Promise<T> {
|
|
26
29
|
let lastError: unknown;
|
|
30
|
+
let lastSignature: string | null = null;
|
|
31
|
+
let sameSignatureCount = 0;
|
|
27
32
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
28
33
|
try {
|
|
29
34
|
return await fn();
|
|
30
35
|
} catch (e) {
|
|
31
36
|
lastError = e;
|
|
32
37
|
if (attempt >= retries || !isTransientError(e)) throw e;
|
|
33
|
-
|
|
38
|
+
const signature = getErrorSignature(e);
|
|
39
|
+
if (signature === lastSignature) {
|
|
40
|
+
sameSignatureCount += 1;
|
|
41
|
+
} else {
|
|
42
|
+
lastSignature = signature;
|
|
43
|
+
sameSignatureCount = 0;
|
|
44
|
+
}
|
|
45
|
+
if (sameSignatureCount >= 1) {
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
48
|
+
const decision = getRetryDecision(e);
|
|
49
|
+
const delay = decision.retryAfterMs ?? (baseDelayMs * Math.max(1, attempt + 1));
|
|
50
|
+
await sleep(delay);
|
|
34
51
|
}
|
|
35
52
|
}
|
|
36
53
|
throw lastError instanceof Error ? lastError : new Error('Request failed');
|
|
@@ -60,7 +77,7 @@ function createCloudOllamaClient(apiKey: string): Ollama {
|
|
|
60
77
|
} as any);
|
|
61
78
|
}
|
|
62
79
|
|
|
63
|
-
async function ensureOllamaServe(
|
|
80
|
+
async function ensureOllamaServe(_apiKey?: string): Promise<void> {
|
|
64
81
|
if (serveStartPromise) return serveStartPromise;
|
|
65
82
|
|
|
66
83
|
serveStartPromise = (async () => {
|
|
@@ -197,9 +214,9 @@ async function ensureOllamaModelAvailable(ollamaClient: Ollama, model: string):
|
|
|
197
214
|
return p;
|
|
198
215
|
}
|
|
199
216
|
|
|
200
|
-
function contentToString(content: CoreMessage['content']): string {
|
|
201
|
-
if (typeof content === 'string') return content;
|
|
202
|
-
if (!content) return '';
|
|
217
|
+
function contentToString(content: CoreMessage['content']): string {
|
|
218
|
+
if (typeof content === 'string') return content;
|
|
219
|
+
if (!content) return '';
|
|
203
220
|
|
|
204
221
|
if (Array.isArray(content)) {
|
|
205
222
|
const text = content
|
|
@@ -218,17 +235,17 @@ function contentToString(content: CoreMessage['content']): string {
|
|
|
218
235
|
return JSON.stringify(content);
|
|
219
236
|
} catch {
|
|
220
237
|
return String(content);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function imagePartToBase64(image: any): string | undefined {
|
|
225
|
-
if (!image) return undefined;
|
|
226
|
-
if (typeof image === 'string') return image;
|
|
227
|
-
if (Buffer.isBuffer(image)) return image.toString('base64');
|
|
228
|
-
if (image instanceof Uint8Array) return Buffer.from(image).toString('base64');
|
|
229
|
-
if (image instanceof ArrayBuffer) return Buffer.from(new Uint8Array(image)).toString('base64');
|
|
230
|
-
return undefined;
|
|
231
|
-
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function imagePartToBase64(image: any): string | undefined {
|
|
242
|
+
if (!image) return undefined;
|
|
243
|
+
if (typeof image === 'string') return image;
|
|
244
|
+
if (Buffer.isBuffer(image)) return image.toString('base64');
|
|
245
|
+
if (image instanceof Uint8Array) return Buffer.from(image).toString('base64');
|
|
246
|
+
if (image instanceof ArrayBuffer) return Buffer.from(new Uint8Array(image)).toString('base64');
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
232
249
|
|
|
233
250
|
function toOllamaTools(tools?: Record<string, CoreTool>): any[] | undefined {
|
|
234
251
|
if (!tools) return undefined;
|
|
@@ -258,10 +275,10 @@ function toOllamaTools(tools?: Record<string, CoreTool>): any[] | undefined {
|
|
|
258
275
|
});
|
|
259
276
|
}
|
|
260
277
|
|
|
261
|
-
function coreMessagesToOllamaMessages(messages: CoreMessage[]): any[] {
|
|
262
|
-
return messages
|
|
263
|
-
.map((message) => {
|
|
264
|
-
if (message.role === 'tool') {
|
|
278
|
+
function coreMessagesToOllamaMessages(messages: CoreMessage[]): any[] {
|
|
279
|
+
return messages
|
|
280
|
+
.map((message) => {
|
|
281
|
+
if (message.role === 'tool') {
|
|
265
282
|
const content: any = message.content;
|
|
266
283
|
const part = Array.isArray(content) ? content?.[0] : undefined;
|
|
267
284
|
const toolName = part?.toolName ?? part?.tool_name;
|
|
@@ -292,28 +309,28 @@ function coreMessagesToOllamaMessages(messages: CoreMessage[]): any[] {
|
|
|
292
309
|
};
|
|
293
310
|
}
|
|
294
311
|
|
|
295
|
-
if (message.role === 'user' && Array.isArray(message.content)) {
|
|
296
|
-
const textParts = message.content
|
|
297
|
-
.map((part: any) => (part && typeof part.text === 'string' ? part.text : ''))
|
|
298
|
-
.filter(Boolean)
|
|
299
|
-
.join('');
|
|
300
|
-
const images = message.content
|
|
301
|
-
.map((part: any) => (part && part.type === 'image' ? imagePartToBase64(part.image) : undefined))
|
|
302
|
-
.filter(Boolean);
|
|
303
|
-
const msg: any = { role: 'user', content: textParts };
|
|
304
|
-
if (images.length > 0) {
|
|
305
|
-
msg.images = images;
|
|
306
|
-
}
|
|
307
|
-
return msg;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return {
|
|
311
|
-
role: message.role,
|
|
312
|
-
content: contentToString(message.content),
|
|
313
|
-
};
|
|
314
|
-
})
|
|
315
|
-
.filter(Boolean);
|
|
316
|
-
}
|
|
312
|
+
if (message.role === 'user' && Array.isArray(message.content)) {
|
|
313
|
+
const textParts = message.content
|
|
314
|
+
.map((part: any) => (part && typeof part.text === 'string' ? part.text : ''))
|
|
315
|
+
.filter(Boolean)
|
|
316
|
+
.join('');
|
|
317
|
+
const images = message.content
|
|
318
|
+
.map((part: any) => (part && part.type === 'image' ? imagePartToBase64(part.image) : undefined))
|
|
319
|
+
.filter(Boolean);
|
|
320
|
+
const msg: any = { role: 'user', content: textParts };
|
|
321
|
+
if (images.length > 0) {
|
|
322
|
+
msg.images = images;
|
|
323
|
+
}
|
|
324
|
+
return msg;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
role: message.role,
|
|
329
|
+
content: contentToString(message.content),
|
|
330
|
+
};
|
|
331
|
+
})
|
|
332
|
+
.filter(Boolean);
|
|
333
|
+
}
|
|
317
334
|
|
|
318
335
|
export async function checkAndStartOllama(): Promise<{ running: boolean; started: boolean; error?: string }> {
|
|
319
336
|
const ollamaClient = new Ollama();
|
|
@@ -344,9 +361,9 @@ export class OllamaProvider implements Provider {
|
|
|
344
361
|
config: ProviderConfig,
|
|
345
362
|
options?: ProviderSendOptions
|
|
346
363
|
): AsyncGenerator<AgentEvent> {
|
|
347
|
-
const apiKey = config.apiKey?.trim().replace(/[\r\n]+/g, '');
|
|
348
|
-
const cleanModel = config.model.trim().replace(/[\r\n]+/g, '');
|
|
349
|
-
const reasoningEnabled = await shouldEnableReasoning(config.provider, cleanModel);
|
|
364
|
+
const apiKey = config.apiKey?.trim().replace(/[\r\n]+/g, '');
|
|
365
|
+
const cleanModel = config.model.trim().replace(/[\r\n]+/g, '');
|
|
366
|
+
const reasoningEnabled = await shouldEnableReasoning(config.provider, cleanModel);
|
|
350
367
|
|
|
351
368
|
if (options?.abortSignal?.aborted) {
|
|
352
369
|
return;
|
|
@@ -413,7 +430,7 @@ export class OllamaProvider implements Provider {
|
|
|
413
430
|
}
|
|
414
431
|
|
|
415
432
|
const toolsSchema = toOllamaTools(config.tools);
|
|
416
|
-
const maxSteps = config.maxSteps ||
|
|
433
|
+
const maxSteps = config.maxSteps || 100;
|
|
417
434
|
|
|
418
435
|
const baseMessages = config.systemPrompt
|
|
419
436
|
? [{ role: 'system' as const, content: config.systemPrompt }, ...messages]
|
|
@@ -435,14 +452,14 @@ export class OllamaProvider implements Provider {
|
|
|
435
452
|
|
|
436
453
|
const stream = await retry(
|
|
437
454
|
() =>
|
|
438
|
-
ollamaClient.chat({
|
|
439
|
-
model: requestModel,
|
|
440
|
-
messages: ollamaMessages,
|
|
441
|
-
tools: toolsSchema,
|
|
442
|
-
stream: true,
|
|
443
|
-
think: reasoningEnabled,
|
|
444
|
-
signal: options?.abortSignal,
|
|
445
|
-
} as any) as any,
|
|
455
|
+
ollamaClient.chat({
|
|
456
|
+
model: requestModel,
|
|
457
|
+
messages: ollamaMessages,
|
|
458
|
+
tools: toolsSchema,
|
|
459
|
+
stream: true,
|
|
460
|
+
think: reasoningEnabled,
|
|
461
|
+
signal: options?.abortSignal,
|
|
462
|
+
} as any) as any,
|
|
446
463
|
2,
|
|
447
464
|
500
|
|
448
465
|
);
|
|
@@ -567,4 +584,4 @@ export class OllamaProvider implements Provider {
|
|
|
567
584
|
};
|
|
568
585
|
}
|
|
569
586
|
}
|
|
570
|
-
}
|
|
587
|
+
}
|
|
@@ -3,6 +3,7 @@ import { createOpenAI } from '@ai-sdk/openai';
|
|
|
3
3
|
import { AgentEvent, Provider, ProviderConfig, ProviderSendOptions } from '../types';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { shouldEnableReasoning } from './reasoning';
|
|
6
|
+
import { getRetryDecision, normalizeError, runWithRetry } from './rateLimit';
|
|
6
7
|
|
|
7
8
|
function unwrapOptional(schema: z.ZodTypeAny): z.ZodTypeAny {
|
|
8
9
|
if (schema instanceof z.ZodOptional) {
|
|
@@ -103,10 +104,10 @@ export class OpenAIProvider implements Provider {
|
|
|
103
104
|
}
|
|
104
105
|
};
|
|
105
106
|
|
|
106
|
-
const
|
|
107
|
-
endpoint: OpenAIEndpoint,
|
|
108
|
-
strictJsonSchema: boolean
|
|
109
|
-
): AsyncGenerator<AgentEvent> {
|
|
107
|
+
const runOnce = async function* (
|
|
108
|
+
endpoint: OpenAIEndpoint,
|
|
109
|
+
strictJsonSchema: boolean
|
|
110
|
+
): AsyncGenerator<AgentEvent> {
|
|
110
111
|
const toolsToUse =
|
|
111
112
|
endpoint === 'responses'
|
|
112
113
|
? transformToolsForResponsesApi(config.tools)
|
|
@@ -117,7 +118,7 @@ export class OpenAIProvider implements Provider {
|
|
|
117
118
|
messages: messages,
|
|
118
119
|
system: config.systemPrompt,
|
|
119
120
|
tools: toolsToUse,
|
|
120
|
-
maxSteps: config.maxSteps ??
|
|
121
|
+
maxSteps: config.maxSteps ?? 100,
|
|
121
122
|
abortSignal: options?.abortSignal,
|
|
122
123
|
providerOptions: {
|
|
123
124
|
openai: {
|
|
@@ -191,26 +192,23 @@ export class OpenAIProvider implements Provider {
|
|
|
191
192
|
};
|
|
192
193
|
break;
|
|
193
194
|
|
|
194
|
-
case 'error':
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return;
|
|
213
|
-
};
|
|
195
|
+
case 'error': {
|
|
196
|
+
const err = normalizeError(c.error);
|
|
197
|
+
const decision = getRetryDecision(err);
|
|
198
|
+
if (decision.shouldRetry) {
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
201
|
+
yield {
|
|
202
|
+
type: 'error',
|
|
203
|
+
error: err.message,
|
|
204
|
+
};
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return;
|
|
211
|
+
};
|
|
214
212
|
|
|
215
213
|
const classifyEndpointError = (msg: string): OpenAIEndpoint | null => {
|
|
216
214
|
const m = msg || '';
|
|
@@ -230,19 +228,25 @@ export class OpenAIProvider implements Provider {
|
|
|
230
228
|
};
|
|
231
229
|
|
|
232
230
|
try {
|
|
233
|
-
yield*
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
231
|
+
yield* runWithRetry(
|
|
232
|
+
() => runOnce('responses', false),
|
|
233
|
+
{ abortSignal: options?.abortSignal }
|
|
234
|
+
);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (options?.abortSignal?.aborted) return;
|
|
237
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
238
|
+
|
|
239
|
+
const fallbackEndpoint = classifyEndpointError(msg);
|
|
240
|
+
if (fallbackEndpoint && fallbackEndpoint !== 'responses') {
|
|
241
|
+
try {
|
|
242
|
+
yield* runWithRetry(
|
|
243
|
+
() => runOnce(fallbackEndpoint, false),
|
|
244
|
+
{ abortSignal: options?.abortSignal }
|
|
245
|
+
);
|
|
246
|
+
return;
|
|
247
|
+
} catch (endpointError) {
|
|
248
|
+
if (options?.abortSignal?.aborted) return;
|
|
249
|
+
const endpointMsg = endpointError instanceof Error ? endpointError.message : String(endpointError);
|
|
246
250
|
yield {
|
|
247
251
|
type: 'error',
|
|
248
252
|
error: endpointMsg || 'Unknown error occurred',
|