@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.
Files changed (75) hide show
  1. package/README.md +1 -5
  2. package/package.json +4 -2
  3. package/src/agent/Agent.ts +353 -131
  4. package/src/agent/context.ts +4 -4
  5. package/src/agent/prompts/systemPrompt.ts +15 -6
  6. package/src/agent/prompts/toolsPrompt.ts +75 -10
  7. package/src/agent/provider/anthropic.ts +100 -100
  8. package/src/agent/provider/google.ts +102 -102
  9. package/src/agent/provider/mistral.ts +95 -95
  10. package/src/agent/provider/ollama.ts +77 -60
  11. package/src/agent/provider/openai.ts +42 -38
  12. package/src/agent/provider/rateLimit.ts +178 -0
  13. package/src/agent/provider/xai.ts +99 -99
  14. package/src/agent/tools/definitions.ts +19 -9
  15. package/src/agent/tools/executor.ts +95 -85
  16. package/src/agent/tools/exploreExecutor.ts +8 -10
  17. package/src/agent/tools/grep.ts +30 -29
  18. package/src/agent/tools/question.ts +7 -1
  19. package/src/agent/types.ts +9 -8
  20. package/src/components/App.tsx +45 -45
  21. package/src/components/CustomInput.tsx +214 -36
  22. package/src/components/Main.tsx +1146 -954
  23. package/src/components/Setup.tsx +1 -1
  24. package/src/components/Welcome.tsx +1 -1
  25. package/src/components/main/ApprovalPanel.tsx +4 -3
  26. package/src/components/main/ChatPage.tsx +858 -675
  27. package/src/components/main/HomePage.tsx +53 -38
  28. package/src/components/main/QuestionPanel.tsx +52 -7
  29. package/src/components/main/ThinkingIndicator.tsx +2 -1
  30. package/src/index.tsx +50 -20
  31. package/src/mcp/approvalPolicy.ts +148 -0
  32. package/src/mcp/cli/add.ts +185 -0
  33. package/src/mcp/cli/doctor.ts +77 -0
  34. package/src/mcp/cli/index.ts +85 -0
  35. package/src/mcp/cli/list.ts +50 -0
  36. package/src/mcp/cli/logs.ts +24 -0
  37. package/src/mcp/cli/manage.ts +99 -0
  38. package/src/mcp/cli/show.ts +53 -0
  39. package/src/mcp/cli/tools.ts +77 -0
  40. package/src/mcp/config.ts +223 -0
  41. package/src/mcp/index.ts +80 -0
  42. package/src/mcp/processManager.ts +299 -0
  43. package/src/mcp/rateLimiter.ts +50 -0
  44. package/src/mcp/registry.ts +151 -0
  45. package/src/mcp/schemaConverter.ts +100 -0
  46. package/src/mcp/servers/navigation.ts +854 -0
  47. package/src/mcp/toolCatalog.ts +169 -0
  48. package/src/mcp/types.ts +95 -0
  49. package/src/utils/approvalBridge.ts +17 -5
  50. package/src/utils/commands/compact.ts +30 -0
  51. package/src/utils/commands/echo.ts +1 -1
  52. package/src/utils/commands/index.ts +4 -6
  53. package/src/utils/commands/new.ts +15 -0
  54. package/src/utils/commands/types.ts +3 -0
  55. package/src/utils/config.ts +3 -1
  56. package/src/utils/diffRendering.tsx +1 -3
  57. package/src/utils/exploreBridge.ts +10 -0
  58. package/src/utils/markdown.tsx +163 -99
  59. package/src/utils/models.ts +31 -9
  60. package/src/utils/questionBridge.ts +36 -1
  61. package/src/utils/tokenEstimator.ts +32 -0
  62. package/src/utils/toolFormatting.ts +268 -7
  63. package/src/web/app.tsx +72 -72
  64. package/src/web/components/HomePage.tsx +7 -7
  65. package/src/web/components/MessageItem.tsx +22 -22
  66. package/src/web/components/QuestionPanel.tsx +72 -12
  67. package/src/web/components/Sidebar.tsx +0 -2
  68. package/src/web/components/ThinkingIndicator.tsx +1 -0
  69. package/src/web/server.tsx +767 -683
  70. package/src/utils/commands/redo.ts +0 -74
  71. package/src/utils/commands/sessions.ts +0 -129
  72. package/src/utils/commands/undo.ts +0 -75
  73. package/src/utils/undoRedo.ts +0 -429
  74. package/src/utils/undoRedoBridge.ts +0 -45
  75. 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
- const result = streamText({
19
- model: mistral(cleanModel),
20
- messages: messages,
21
- system: config.systemPrompt,
22
- tools: config.tools,
23
- maxSteps: config.maxSteps || 10,
24
- abortSignal: options?.abortSignal
25
- });
26
-
27
- try {
28
- let stepCounter = 0;
29
-
30
- for await (const chunk of result.fullStream as any) {
31
- const c: any = chunk;
32
- switch (c.type) {
33
- case 'reasoning':
34
- if (c.textDelta) {
35
- yield {
36
- type: 'reasoning-delta',
37
- content: c.textDelta,
38
- };
39
- }
40
- break;
41
-
42
- case 'text-delta':
43
- yield {
44
- type: 'text-delta',
45
- content: c.textDelta,
46
- };
47
- break;
48
-
49
- case 'step-start':
50
- yield {
51
- type: 'step-start',
52
- stepNumber: typeof c.stepIndex === 'number' ? c.stepIndex : stepCounter,
53
- };
54
- stepCounter++;
55
- break;
56
-
57
- case 'step-finish':
58
- yield {
59
- type: 'step-finish',
60
- stepNumber:
61
- typeof c.stepIndex === 'number' ? c.stepIndex : Math.max(0, stepCounter - 1),
62
- finishReason: String(c.finishReason ?? 'stop'),
63
- };
64
- break;
65
-
66
- case 'tool-call':
67
- yield {
68
- type: 'tool-call-end',
69
- toolCallId: String(c.toolCallId ?? ''),
70
- toolName: String(c.toolName ?? ''),
71
- args: (c.args ?? {}) as Record<string, unknown>,
72
- };
73
- break;
74
-
75
- case 'tool-result':
76
- yield {
77
- type: 'tool-result',
78
- toolCallId: String(c.toolCallId ?? ''),
79
- toolName: String(c.toolName ?? ''),
80
- result: c.result,
81
- };
82
- break;
83
-
84
- case 'finish':
85
- yield {
86
- type: 'finish',
87
- finishReason: String(c.finishReason ?? 'stop'),
88
- usage: c.usage,
89
- };
90
- break;
91
-
92
- case 'error':
93
- {
94
- const err = c.error;
95
- const msg =
96
- err instanceof Error
97
- ? err.message
98
- : typeof err === 'string'
99
- ? err
100
- : 'Unknown error';
101
- yield {
102
- type: 'error',
103
- error: msg,
104
- };
105
- }
106
- break;
107
- }
108
- }
109
- } catch (error) {
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
- await sleep(baseDelayMs * Math.max(1, attempt + 1));
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(apiKey?: string): Promise<void> {
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 || 10;
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 run = async function* (
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 ?? 10,
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
- const err = c.error;
197
- const msg =
198
- err instanceof Error
199
- ? err.message
200
- : typeof err === 'string'
201
- ? err
202
- : 'Unknown error';
203
- yield {
204
- type: 'error',
205
- error: msg,
206
- };
207
- }
208
- break;
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* run('responses', false);
234
- } catch (error) {
235
- if (options?.abortSignal?.aborted) return;
236
- const msg = error instanceof Error ? error.message : String(error);
237
-
238
- const fallbackEndpoint = classifyEndpointError(msg);
239
- if (fallbackEndpoint && fallbackEndpoint !== 'responses') {
240
- try {
241
- yield* run(fallbackEndpoint, false);
242
- return;
243
- } catch (endpointError) {
244
- if (options?.abortSignal?.aborted) return;
245
- const endpointMsg = endpointError instanceof Error ? endpointError.message : String(endpointError);
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',