@kirosnn/mosaic 0.0.7
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/.mosaic/mosaic.local.jsonc +0 -0
- package/MOSAIC.md +188 -0
- package/README.md +127 -0
- package/docs/mosaic.png +0 -0
- package/package.json +42 -0
- package/src/agent/Agent.ts +131 -0
- package/src/agent/context.ts +96 -0
- package/src/agent/index.ts +2 -0
- package/src/agent/prompts/systemPrompt.ts +138 -0
- package/src/agent/prompts/toolsPrompt.ts +139 -0
- package/src/agent/provider/anthropic.ts +122 -0
- package/src/agent/provider/google.ts +124 -0
- package/src/agent/provider/mistral.ts +117 -0
- package/src/agent/provider/ollama.ts +531 -0
- package/src/agent/provider/openai.ts +220 -0
- package/src/agent/provider/xai.ts +122 -0
- package/src/agent/tools/bash.ts +20 -0
- package/src/agent/tools/definitions.ts +27 -0
- package/src/agent/tools/edit.ts +23 -0
- package/src/agent/tools/executor.ts +751 -0
- package/src/agent/tools/explore.ts +18 -0
- package/src/agent/tools/exploreExecutor.ts +320 -0
- package/src/agent/tools/glob.ts +16 -0
- package/src/agent/tools/grep.ts +19 -0
- package/src/agent/tools/index.ts +4 -0
- package/src/agent/tools/list.ts +20 -0
- package/src/agent/tools/question.ts +20 -0
- package/src/agent/tools/read.ts +15 -0
- package/src/agent/tools/write.ts +21 -0
- package/src/agent/types.ts +155 -0
- package/src/components/App.tsx +174 -0
- package/src/components/CommandsModal.tsx +77 -0
- package/src/components/CustomInput.tsx +328 -0
- package/src/components/Main.tsx +1112 -0
- package/src/components/Notification.tsx +91 -0
- package/src/components/SelectList.tsx +47 -0
- package/src/components/Setup.tsx +528 -0
- package/src/components/ShortcutsModal.tsx +67 -0
- package/src/components/Welcome.tsx +39 -0
- package/src/components/main/ApprovalPanel.tsx +134 -0
- package/src/components/main/ChatPage.tsx +516 -0
- package/src/components/main/HomePage.tsx +111 -0
- package/src/components/main/QuestionPanel.tsx +85 -0
- package/src/components/main/ThinkingIndicator.tsx +101 -0
- package/src/components/main/types.ts +55 -0
- package/src/components/main/wrapText.ts +41 -0
- package/src/index.tsx +212 -0
- package/src/utils/approvalBridge.ts +129 -0
- package/src/utils/commands/echo.ts +22 -0
- package/src/utils/commands/help.ts +25 -0
- package/src/utils/commands/index.ts +68 -0
- package/src/utils/commands/init.ts +68 -0
- package/src/utils/commands/redo.ts +74 -0
- package/src/utils/commands/registry.ts +29 -0
- package/src/utils/commands/sessions.ts +129 -0
- package/src/utils/commands/types.ts +20 -0
- package/src/utils/commands/undo.ts +75 -0
- package/src/utils/commands/web.ts +77 -0
- package/src/utils/config.ts +357 -0
- package/src/utils/diff.ts +201 -0
- package/src/utils/diffRendering.tsx +62 -0
- package/src/utils/exploreBridge.ts +87 -0
- package/src/utils/fileChangeTracker.ts +98 -0
- package/src/utils/fileChangesBridge.ts +18 -0
- package/src/utils/history.ts +106 -0
- package/src/utils/markdown.tsx +232 -0
- package/src/utils/models.ts +304 -0
- package/src/utils/questionBridge.ts +122 -0
- package/src/utils/terminalUtils.ts +25 -0
- package/src/utils/toolFormatting.ts +384 -0
- package/src/utils/undoRedo.ts +429 -0
- package/src/utils/undoRedoBridge.ts +45 -0
- package/src/utils/undoRedoDb.ts +338 -0
- package/src/utils/uninstall.ts +45 -0
- package/src/utils/version.ts +3 -0
- package/src/web/app.tsx +606 -0
- package/src/web/assets/css/ChatPage.css +212 -0
- package/src/web/assets/css/FileExplorer.css +202 -0
- package/src/web/assets/css/HomePage.css +119 -0
- package/src/web/assets/css/Markdown.css +178 -0
- package/src/web/assets/css/MessageItem.css +160 -0
- package/src/web/assets/css/Sidebar.css +208 -0
- package/src/web/assets/css/SidebarModal.css +137 -0
- package/src/web/assets/css/ThinkingIndicator.css +47 -0
- package/src/web/assets/css/ToolMessage.css +148 -0
- package/src/web/assets/css/global.css +226 -0
- package/src/web/assets/fonts/Geist-Black.woff2 +0 -0
- package/src/web/assets/fonts/Geist-BlackItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Bold.woff2 +0 -0
- package/src/web/assets/fonts/Geist-BoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraBold.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraLight.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraLightItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Italic[wght].woff2 +0 -0
- package/src/web/assets/fonts/Geist-Light.woff2 +0 -0
- package/src/web/assets/fonts/Geist-LightItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Medium.woff2 +0 -0
- package/src/web/assets/fonts/Geist-MediumItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Regular.woff2 +0 -0
- package/src/web/assets/fonts/Geist-RegularItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-SemiBold.woff2 +0 -0
- package/src/web/assets/fonts/Geist-SemiBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Thin.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ThinItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Black.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-BlackItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Bold.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-BoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraBold.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraLight.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraLightItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Italic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Italic[wght].woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Light.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-LightItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Medium.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-MediumItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Regular.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-SemiBold.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-SemiBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Thin.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ThinItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono[wght].woff2 +0 -0
- package/src/web/assets/fonts/Geist[wght].woff2 +0 -0
- package/src/web/assets/fonts/blauer-nue-regular.woff2 +0 -0
- package/src/web/assets/fonts/neue-montreal-regular.woff2 +0 -0
- package/src/web/assets/images/favicon-v2.svg +6 -0
- package/src/web/assets/images/favicon.png +0 -0
- package/src/web/assets/images/foruse.svg +5 -0
- package/src/web/assets/images/logo_black.svg +5 -0
- package/src/web/assets/images/logo_white.svg +5 -0
- package/src/web/assets/images/logoblack.png +0 -0
- package/src/web/assets/images/logowhite.png +0 -0
- package/src/web/build.ts +23 -0
- package/src/web/components/ApprovalPanel.tsx +191 -0
- package/src/web/components/ChatPage.tsx +273 -0
- package/src/web/components/FileExplorer.tsx +162 -0
- package/src/web/components/HomePage.tsx +121 -0
- package/src/web/components/MessageItem.tsx +178 -0
- package/src/web/components/Modal.tsx +30 -0
- package/src/web/components/QuestionPanel.tsx +149 -0
- package/src/web/components/Setup.tsx +211 -0
- package/src/web/components/Sidebar.tsx +292 -0
- package/src/web/components/ThinkingIndicator.tsx +85 -0
- package/src/web/logo_black.svg +5 -0
- package/src/web/logo_white.svg +5 -0
- package/src/web/router.ts +46 -0
- package/src/web/server.tsx +662 -0
- package/src/web/storage.ts +92 -0
- package/src/web/types.ts +17 -0
- package/src/web/utils.ts +61 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { Ollama } from 'ollama';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { CoreMessage, CoreTool } from 'ai';
|
|
4
|
+
import { AgentEvent, Provider, ProviderConfig, ProviderSendOptions } from '../types';
|
|
5
|
+
|
|
6
|
+
let serveStartPromise: Promise<void> | null = null;
|
|
7
|
+
const pullPromises = new Map<string, Promise<void>>();
|
|
8
|
+
|
|
9
|
+
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
|
|
11
|
+
function isTransientError(error: unknown): boolean {
|
|
12
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
13
|
+
return (
|
|
14
|
+
msg.includes('ECONNREFUSED') ||
|
|
15
|
+
msg.includes('ETIMEDOUT') ||
|
|
16
|
+
msg.includes('fetch failed') ||
|
|
17
|
+
msg.toLowerCase().includes('socket') ||
|
|
18
|
+
msg.includes('500')
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function retry<T>(fn: () => Promise<T>, retries: number, baseDelayMs: number): Promise<T> {
|
|
23
|
+
let lastError: unknown;
|
|
24
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
25
|
+
try {
|
|
26
|
+
return await fn();
|
|
27
|
+
} catch (e) {
|
|
28
|
+
lastError = e;
|
|
29
|
+
if (attempt >= retries || !isTransientError(e)) throw e;
|
|
30
|
+
await sleep(baseDelayMs * Math.max(1, attempt + 1));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
throw lastError instanceof Error ? lastError : new Error('Request failed');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isCloudModel(model: string): boolean {
|
|
37
|
+
return model.endsWith(':cloud') || model.endsWith('-cloud') || model.includes(':cloud') || model.includes('-cloud');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeCloudModelName(model: string): string {
|
|
41
|
+
if (model.endsWith(':cloud')) return model.slice(0, -':cloud'.length);
|
|
42
|
+
if (model.endsWith('-cloud')) return model.slice(0, -'-cloud'.length);
|
|
43
|
+
return model;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createLocalOllamaClient(apiKey?: string): Ollama {
|
|
47
|
+
void apiKey;
|
|
48
|
+
return new Ollama();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createCloudOllamaClient(apiKey: string): Ollama {
|
|
52
|
+
return new Ollama({
|
|
53
|
+
host: 'https://ollama.com',
|
|
54
|
+
headers: {
|
|
55
|
+
Authorization: `Bearer ${apiKey}`,
|
|
56
|
+
},
|
|
57
|
+
} as any);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function ensureOllamaServe(apiKey?: string): Promise<void> {
|
|
61
|
+
if (serveStartPromise) return serveStartPromise;
|
|
62
|
+
|
|
63
|
+
serveStartPromise = (async () => {
|
|
64
|
+
try {
|
|
65
|
+
const child = spawn('ollama', ['serve'], {
|
|
66
|
+
detached: true,
|
|
67
|
+
windowsHide: true,
|
|
68
|
+
env: {
|
|
69
|
+
...process.env,
|
|
70
|
+
},
|
|
71
|
+
stdio: 'ignore',
|
|
72
|
+
});
|
|
73
|
+
child.unref();
|
|
74
|
+
} catch {
|
|
75
|
+
// ignore
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await sleep(500);
|
|
79
|
+
})();
|
|
80
|
+
|
|
81
|
+
return serveStartPromise;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function ollamaVersion(ollamaClient: Ollama): Promise<unknown> {
|
|
85
|
+
const versionFn = (ollamaClient as any)?.version;
|
|
86
|
+
if (typeof versionFn === 'function') {
|
|
87
|
+
return versionFn.call(ollamaClient);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const host = String((ollamaClient as any)?.host ?? 'http://127.0.0.1:11434').replace(/\/$/, '');
|
|
91
|
+
const headers = (ollamaClient as any)?.headers ?? {};
|
|
92
|
+
|
|
93
|
+
const res = await fetch(`${host}/api/version`, {
|
|
94
|
+
headers,
|
|
95
|
+
} as any);
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
throw new Error(`Ollama server not reachable (status ${res.status})`);
|
|
98
|
+
}
|
|
99
|
+
return res.json();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function ensureOllamaReachableLocal(ollamaClient: Ollama, apiKey?: string): Promise<void> {
|
|
103
|
+
try {
|
|
104
|
+
await ollamaVersion(ollamaClient);
|
|
105
|
+
return;
|
|
106
|
+
} catch {
|
|
107
|
+
// fallthrough
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await ensureOllamaServe(apiKey);
|
|
111
|
+
|
|
112
|
+
await retry(() => ollamaVersion(ollamaClient), 6, 350);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function ensureOllamaReachableCloud(ollamaClient: Ollama): Promise<void> {
|
|
116
|
+
await retry(() => ollamaVersion(ollamaClient), 2, 400);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function hasLocalModel(ollamaClient: Ollama, model: string): Promise<boolean> {
|
|
120
|
+
try {
|
|
121
|
+
const host = String((ollamaClient as any)?.host ?? 'http://127.0.0.1:11434').replace(/\/$/, '');
|
|
122
|
+
const headers = (ollamaClient as any)?.headers ?? {};
|
|
123
|
+
|
|
124
|
+
const res = await fetch(`${host}/api/tags`, {
|
|
125
|
+
headers,
|
|
126
|
+
} as any);
|
|
127
|
+
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const data = await res.json();
|
|
133
|
+
const models = ((data as any)?.models ?? []) as Array<Record<string, any>>;
|
|
134
|
+
return models.some((m) => m?.name === model || m?.model === model);
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function ensureOllamaModelAvailable(ollamaClient: Ollama, model: string): Promise<void> {
|
|
141
|
+
const existing = pullPromises.get(model);
|
|
142
|
+
if (existing) return existing;
|
|
143
|
+
|
|
144
|
+
const p = (async () => {
|
|
145
|
+
const alreadyPresent = await hasLocalModel(ollamaClient, model);
|
|
146
|
+
if (alreadyPresent) return;
|
|
147
|
+
|
|
148
|
+
const host = String((ollamaClient as any)?.host ?? 'http://127.0.0.1:11434').replace(/\/$/, '');
|
|
149
|
+
const headers = (ollamaClient as any)?.headers ?? {};
|
|
150
|
+
|
|
151
|
+
const res = await fetch(`${host}/api/pull`, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: {
|
|
154
|
+
...headers,
|
|
155
|
+
'Content-Type': 'application/json',
|
|
156
|
+
},
|
|
157
|
+
body: JSON.stringify({
|
|
158
|
+
model,
|
|
159
|
+
stream: true,
|
|
160
|
+
}),
|
|
161
|
+
} as any);
|
|
162
|
+
|
|
163
|
+
if (!res.ok) {
|
|
164
|
+
throw new Error(`Failed to pull model ${model} (status ${res.status})`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (res.body) {
|
|
168
|
+
const reader = res.body.getReader();
|
|
169
|
+
const decoder = new TextDecoder();
|
|
170
|
+
|
|
171
|
+
while (true) {
|
|
172
|
+
const { done, value } = await reader.read();
|
|
173
|
+
if (done) break;
|
|
174
|
+
|
|
175
|
+
const chunk = decoder.decode(value);
|
|
176
|
+
const lines = chunk.split('\n').filter(Boolean);
|
|
177
|
+
|
|
178
|
+
for (const line of lines) {
|
|
179
|
+
try {
|
|
180
|
+
const data = JSON.parse(line);
|
|
181
|
+
if (data.error) {
|
|
182
|
+
throw new Error(data.error);
|
|
183
|
+
}
|
|
184
|
+
} catch (e) {
|
|
185
|
+
if (e instanceof SyntaxError) continue;
|
|
186
|
+
throw e;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
})();
|
|
192
|
+
|
|
193
|
+
pullPromises.set(model, p);
|
|
194
|
+
return p;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function contentToString(content: CoreMessage['content']): string {
|
|
198
|
+
if (typeof content === 'string') return content;
|
|
199
|
+
if (!content) return '';
|
|
200
|
+
|
|
201
|
+
if (Array.isArray(content)) {
|
|
202
|
+
const text = content
|
|
203
|
+
.map((part: any) => {
|
|
204
|
+
if (part && typeof part.text === 'string') return part.text;
|
|
205
|
+
if (typeof part === 'string') return part;
|
|
206
|
+
return '';
|
|
207
|
+
})
|
|
208
|
+
.filter(Boolean)
|
|
209
|
+
.join('');
|
|
210
|
+
|
|
211
|
+
if (text) return text;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
return JSON.stringify(content);
|
|
216
|
+
} catch {
|
|
217
|
+
return String(content);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function toOllamaTools(tools?: Record<string, CoreTool>): any[] | undefined {
|
|
222
|
+
if (!tools) return undefined;
|
|
223
|
+
|
|
224
|
+
return Object.entries(tools).map(([name, tool]) => ({
|
|
225
|
+
type: 'function',
|
|
226
|
+
function: {
|
|
227
|
+
name,
|
|
228
|
+
description: String((tool as any)?.description ?? name),
|
|
229
|
+
parameters: (() => {
|
|
230
|
+
const params = (tool as any)?.parameters;
|
|
231
|
+
if (params && typeof params === 'object' && 'type' in params) return params;
|
|
232
|
+
return { type: 'object', properties: {} };
|
|
233
|
+
})(),
|
|
234
|
+
},
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function coreMessagesToOllamaMessages(messages: CoreMessage[]): any[] {
|
|
239
|
+
return messages
|
|
240
|
+
.map((message) => {
|
|
241
|
+
if (message.role === 'tool') {
|
|
242
|
+
const content: any = message.content;
|
|
243
|
+
const part = Array.isArray(content) ? content?.[0] : undefined;
|
|
244
|
+
const toolName = part?.toolName ?? part?.tool_name;
|
|
245
|
+
const result = part?.result;
|
|
246
|
+
|
|
247
|
+
if (toolName) {
|
|
248
|
+
return {
|
|
249
|
+
role: 'tool',
|
|
250
|
+
tool_name: String(toolName),
|
|
251
|
+
content:
|
|
252
|
+
typeof result === 'string'
|
|
253
|
+
? result
|
|
254
|
+
: result == null
|
|
255
|
+
? ''
|
|
256
|
+
: (() => {
|
|
257
|
+
try {
|
|
258
|
+
return JSON.stringify(result);
|
|
259
|
+
} catch {
|
|
260
|
+
return String(result);
|
|
261
|
+
}
|
|
262
|
+
})(),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
role: 'tool',
|
|
268
|
+
content: contentToString(message.content),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
role: message.role,
|
|
274
|
+
content: contentToString(message.content),
|
|
275
|
+
};
|
|
276
|
+
})
|
|
277
|
+
.filter(Boolean);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function checkAndStartOllama(): Promise<{ running: boolean; started: boolean; error?: string }> {
|
|
281
|
+
const ollamaClient = new Ollama();
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
await ollamaVersion(ollamaClient);
|
|
285
|
+
return { running: true, started: false };
|
|
286
|
+
} catch {
|
|
287
|
+
// Ollama is not running, try to start it
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
await ensureOllamaServe();
|
|
292
|
+
await retry(() => ollamaVersion(ollamaClient), 6, 350);
|
|
293
|
+
return { running: true, started: true };
|
|
294
|
+
} catch (e) {
|
|
295
|
+
return {
|
|
296
|
+
running: false,
|
|
297
|
+
started: false,
|
|
298
|
+
error: e instanceof Error ? e.message : 'Failed to start Ollama',
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export class OllamaProvider implements Provider {
|
|
304
|
+
async *sendMessage(
|
|
305
|
+
messages: CoreMessage[],
|
|
306
|
+
config: ProviderConfig,
|
|
307
|
+
options?: ProviderSendOptions
|
|
308
|
+
): AsyncGenerator<AgentEvent> {
|
|
309
|
+
const apiKey = config.apiKey?.trim().replace(/[\r\n]+/g, '');
|
|
310
|
+
const cleanModel = config.model.trim().replace(/[\r\n]+/g, '');
|
|
311
|
+
|
|
312
|
+
if (options?.abortSignal?.aborted) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let ollamaClient: Ollama;
|
|
317
|
+
let requestModel: string;
|
|
318
|
+
|
|
319
|
+
if (isCloudModel(cleanModel) && apiKey) {
|
|
320
|
+
ollamaClient = createCloudOllamaClient(apiKey);
|
|
321
|
+
requestModel = normalizeCloudModelName(cleanModel);
|
|
322
|
+
try {
|
|
323
|
+
await ensureOllamaReachableCloud(ollamaClient);
|
|
324
|
+
} catch (cloudError) {
|
|
325
|
+
yield {
|
|
326
|
+
type: 'error',
|
|
327
|
+
error:
|
|
328
|
+
(cloudError instanceof Error ? cloudError.message : 'Failed to reach Ollama cloud API') +
|
|
329
|
+
' (check your Ollama API key and cloud access).',
|
|
330
|
+
};
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
ollamaClient = createLocalOllamaClient(apiKey);
|
|
335
|
+
requestModel = cleanModel;
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
await ensureOllamaReachableLocal(ollamaClient, apiKey);
|
|
339
|
+
|
|
340
|
+
const present = await hasLocalModel(ollamaClient, cleanModel);
|
|
341
|
+
if (!present) {
|
|
342
|
+
await ensureOllamaModelAvailable(ollamaClient, cleanModel);
|
|
343
|
+
}
|
|
344
|
+
} catch (localError) {
|
|
345
|
+
if (apiKey) {
|
|
346
|
+
requestModel = normalizeCloudModelName(cleanModel);
|
|
347
|
+
ollamaClient = createCloudOllamaClient(apiKey);
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
await ensureOllamaReachableCloud(ollamaClient);
|
|
351
|
+
} catch (cloudError) {
|
|
352
|
+
yield {
|
|
353
|
+
type: 'error',
|
|
354
|
+
error:
|
|
355
|
+
(cloudError instanceof Error ? cloudError.message : 'Failed to reach Ollama cloud API') +
|
|
356
|
+
' (check your Ollama API key and cloud access).',
|
|
357
|
+
};
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
const hint = isCloudModel(cleanModel)
|
|
362
|
+
? ' If this is a cloud model, run `ollama signin` then `ollama pull <model>` locally.'
|
|
363
|
+
: '';
|
|
364
|
+
yield {
|
|
365
|
+
type: 'error',
|
|
366
|
+
error:
|
|
367
|
+
(localError instanceof Error ? localError.message : 'Failed to prepare Ollama') +
|
|
368
|
+
' Ensure the Ollama app/server is running. If needed, install the Ollama CLI and ensure it is available in PATH.' +
|
|
369
|
+
hint,
|
|
370
|
+
};
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const toolsSchema = toOllamaTools(config.tools);
|
|
377
|
+
const maxSteps = config.maxSteps || 10;
|
|
378
|
+
|
|
379
|
+
const baseMessages = config.systemPrompt
|
|
380
|
+
? [{ role: 'system' as const, content: config.systemPrompt }, ...messages]
|
|
381
|
+
: messages;
|
|
382
|
+
|
|
383
|
+
const ollamaMessages: any[] = coreMessagesToOllamaMessages(baseMessages);
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
for (let stepNumber = 0; stepNumber < maxSteps; stepNumber++) {
|
|
387
|
+
if (options?.abortSignal?.aborted) return;
|
|
388
|
+
yield {
|
|
389
|
+
type: 'step-start',
|
|
390
|
+
stepNumber,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
let assistantContent = '';
|
|
394
|
+
let assistantThinking = '';
|
|
395
|
+
const toolCalls: any[] = [];
|
|
396
|
+
|
|
397
|
+
const stream = await retry(
|
|
398
|
+
() =>
|
|
399
|
+
ollamaClient.chat({
|
|
400
|
+
model: requestModel,
|
|
401
|
+
messages: ollamaMessages,
|
|
402
|
+
tools: toolsSchema,
|
|
403
|
+
stream: true,
|
|
404
|
+
think: true,
|
|
405
|
+
signal: options?.abortSignal,
|
|
406
|
+
} as any) as any,
|
|
407
|
+
2,
|
|
408
|
+
500
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
for await (const chunk of stream as AsyncGenerator<any>) {
|
|
412
|
+
if (options?.abortSignal?.aborted) return;
|
|
413
|
+
const thinkingDelta = chunk?.message?.thinking;
|
|
414
|
+
if (typeof thinkingDelta === 'string' && thinkingDelta) {
|
|
415
|
+
assistantThinking += thinkingDelta;
|
|
416
|
+
yield {
|
|
417
|
+
type: 'reasoning-delta',
|
|
418
|
+
content: thinkingDelta,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const contentDelta = chunk?.message?.content;
|
|
423
|
+
if (typeof contentDelta === 'string' && contentDelta) {
|
|
424
|
+
assistantContent += contentDelta;
|
|
425
|
+
yield {
|
|
426
|
+
type: 'text-delta',
|
|
427
|
+
content: contentDelta,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const partialToolCalls = chunk?.message?.tool_calls;
|
|
432
|
+
if (Array.isArray(partialToolCalls) && partialToolCalls.length > 0) {
|
|
433
|
+
toolCalls.push(...partialToolCalls);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const assistantMessage: any = {
|
|
438
|
+
role: 'assistant',
|
|
439
|
+
content: assistantContent,
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
if (assistantThinking) assistantMessage.thinking = assistantThinking;
|
|
443
|
+
if (toolCalls.length) assistantMessage.tool_calls = toolCalls;
|
|
444
|
+
ollamaMessages.push(assistantMessage);
|
|
445
|
+
|
|
446
|
+
const normalizedCalls = toolCalls
|
|
447
|
+
.map((c, i) => ({
|
|
448
|
+
index: typeof c?.function?.index === 'number' ? c.function.index : i,
|
|
449
|
+
name: String(c?.function?.name ?? ''),
|
|
450
|
+
arguments: (c?.function?.arguments ?? {}) as Record<string, unknown>,
|
|
451
|
+
}))
|
|
452
|
+
.filter((c) => Boolean(c.name));
|
|
453
|
+
|
|
454
|
+
if (normalizedCalls.length > 0 && config.tools && stepNumber < maxSteps - 1) {
|
|
455
|
+
for (let i = 0; i < normalizedCalls.length; i++) {
|
|
456
|
+
if (options?.abortSignal?.aborted) return;
|
|
457
|
+
const call = normalizedCalls[i]!;
|
|
458
|
+
const toolCallId = `ollama-${stepNumber}-${call.index}-${i}`;
|
|
459
|
+
|
|
460
|
+
yield {
|
|
461
|
+
type: 'tool-call-end',
|
|
462
|
+
toolCallId,
|
|
463
|
+
toolName: call.name,
|
|
464
|
+
args: call.arguments,
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
let toolResult: unknown;
|
|
468
|
+
try {
|
|
469
|
+
const tool: any = (config.tools as any)[call.name];
|
|
470
|
+
if (!tool || typeof tool.execute !== 'function') {
|
|
471
|
+
throw new Error(`Tool not available: ${call.name}`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
toolResult = await tool.execute(call.arguments);
|
|
475
|
+
} catch (e) {
|
|
476
|
+
toolResult = { error: e instanceof Error ? e.message : 'Tool execution failed' };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
yield {
|
|
480
|
+
type: 'tool-result',
|
|
481
|
+
toolCallId,
|
|
482
|
+
toolName: call.name,
|
|
483
|
+
result: toolResult,
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
ollamaMessages.push({
|
|
487
|
+
role: 'tool',
|
|
488
|
+
tool_name: call.name,
|
|
489
|
+
content:
|
|
490
|
+
typeof toolResult === 'string'
|
|
491
|
+
? toolResult
|
|
492
|
+
: toolResult == null
|
|
493
|
+
? ''
|
|
494
|
+
: (() => {
|
|
495
|
+
try {
|
|
496
|
+
return JSON.stringify(toolResult);
|
|
497
|
+
} catch {
|
|
498
|
+
return String(toolResult);
|
|
499
|
+
}
|
|
500
|
+
})(),
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
yield {
|
|
505
|
+
type: 'step-finish',
|
|
506
|
+
stepNumber,
|
|
507
|
+
finishReason: 'tool-calls',
|
|
508
|
+
};
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
yield {
|
|
513
|
+
type: 'step-finish',
|
|
514
|
+
stepNumber,
|
|
515
|
+
finishReason: 'stop',
|
|
516
|
+
};
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
yield {
|
|
521
|
+
type: 'finish',
|
|
522
|
+
finishReason: 'stop',
|
|
523
|
+
};
|
|
524
|
+
} catch (error) {
|
|
525
|
+
yield {
|
|
526
|
+
type: 'error',
|
|
527
|
+
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|