@decido/kernel-bridge 1.0.0 → 4.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.
@@ -1,84 +0,0 @@
1
- /**
2
- * OllamaProvider — Local Ollama LLM provider
3
- *
4
- * Wraps Ollama's REST API (localhost:11434) as an LLMProvider.
5
- * Supports tool-calling via <tool_call> blocks in system prompt.
6
- */
7
-
8
- import type { LLMProvider, ChatMessage, ChatOptions, ChatResult, ProviderStatus } from './LLMProvider';
9
-
10
- // ─── Config ─────────────────────────────────────────────────
11
-
12
- const OLLAMA_BASE_URL = 'http://localhost:11434';
13
- const DEFAULT_MODEL = 'qwen2:latest';
14
-
15
- // ─── Provider Implementation ────────────────────────────────
16
-
17
- export class OllamaProvider implements LLMProvider {
18
- readonly id = 'ollama';
19
- readonly name = 'Ollama (Local)';
20
- readonly requiresApiKey = false;
21
- readonly defaultModel = DEFAULT_MODEL;
22
-
23
- async listModels(): Promise<string[]> {
24
- try {
25
- const res = await fetch(`${OLLAMA_BASE_URL}/api/tags`);
26
- if (!res.ok) return [];
27
- const data = await res.json();
28
- return (data.models || []).map((m: { name: string }) => m.name);
29
- } catch {
30
- return [];
31
- }
32
- }
33
-
34
- async checkStatus(): Promise<ProviderStatus> {
35
- try {
36
- const res = await fetch(`${OLLAMA_BASE_URL}/api/tags`, {
37
- signal: AbortSignal.timeout(2000),
38
- });
39
- return res.ok ? 'available' : 'error';
40
- } catch {
41
- return 'unavailable';
42
- }
43
- }
44
-
45
- async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResult> {
46
- const model = options?.model || this.defaultModel;
47
- const start = Date.now();
48
-
49
- const ollamaMessages = messages.map(m => ({
50
- role: m.role,
51
- content: m.content,
52
- }));
53
-
54
- const res = await fetch(`${OLLAMA_BASE_URL}/api/chat`, {
55
- method: 'POST',
56
- headers: { 'Content-Type': 'application/json' },
57
- body: JSON.stringify({
58
- model,
59
- messages: ollamaMessages,
60
- stream: false,
61
- options: {
62
- temperature: options?.temperature ?? 0.7,
63
- num_predict: options?.maxTokens ?? 512,
64
- },
65
- }),
66
- signal: options?.signal ?? AbortSignal.timeout(30000),
67
- });
68
-
69
- if (!res.ok) {
70
- const errorText = await res.text();
71
- throw new Error(`Ollama API error ${res.status}: ${errorText}`);
72
- }
73
-
74
- const data = await res.json();
75
- const latencyMs = Date.now() - start;
76
-
77
- return {
78
- text: data.message?.content || '(sin respuesta)',
79
- model,
80
- latencyMs,
81
- tokensUsed: data.eval_count,
82
- };
83
- }
84
- }
@@ -1,178 +0,0 @@
1
- /**
2
- * OpenAIProvider — GPT-4o / GPT-4 / GPT-3.5 LLM provider
3
- *
4
- * Supports chat completion with streaming via SSE.
5
- * Compatible with OpenAI API and OpenAI-compatible endpoints (e.g., LM Studio).
6
- */
7
-
8
- import type { LLMProvider, ChatMessage, ChatOptions, ChatResult, ProviderStatus } from './LLMProvider';
9
-
10
- // ─── Config ─────────────────────────────────────────────────
11
-
12
- const OPENAI_API_BASE = 'https://api.openai.com/v1';
13
- const DEFAULT_MODEL = 'gpt-4o';
14
-
15
- const AVAILABLE_MODELS = [
16
- 'gpt-4o',
17
- 'gpt-4o-mini',
18
- 'gpt-4-turbo',
19
- 'gpt-4',
20
- 'gpt-3.5-turbo',
21
- 'o3-mini',
22
- ];
23
-
24
- // ─── Provider Implementation ────────────────────────────────
25
-
26
- export class OpenAIProvider implements LLMProvider {
27
- readonly id = 'openai';
28
- readonly name = 'OpenAI';
29
- readonly requiresApiKey = true;
30
- readonly defaultModel = DEFAULT_MODEL;
31
-
32
- private apiKey: string | null = null;
33
- private baseUrl: string = OPENAI_API_BASE;
34
-
35
- setApiKey(key: string): void {
36
- this.apiKey = key;
37
- }
38
-
39
- /** Override base URL for OpenAI-compatible endpoints */
40
- setBaseUrl(url: string): void {
41
- this.baseUrl = url.replace(/\/$/, '');
42
- }
43
-
44
- async listModels(): Promise<string[]> {
45
- if (!this.apiKey) return AVAILABLE_MODELS;
46
-
47
- try {
48
- const res = await fetch(`${this.baseUrl}/models`, {
49
- headers: { Authorization: `Bearer ${this.apiKey}` },
50
- signal: AbortSignal.timeout(5000),
51
- });
52
- if (!res.ok) return AVAILABLE_MODELS;
53
- const data = await res.json();
54
- const models = (data.data || [])
55
- .map((m: { id: string }) => m.id)
56
- .filter((id: string) => id.startsWith('gpt-') || id.startsWith('o'));
57
- return models.length > 0 ? models : AVAILABLE_MODELS;
58
- } catch {
59
- return AVAILABLE_MODELS;
60
- }
61
- }
62
-
63
- async checkStatus(): Promise<ProviderStatus> {
64
- if (!this.apiKey) return 'unconfigured';
65
-
66
- try {
67
- const res = await fetch(`${this.baseUrl}/models`, {
68
- headers: { Authorization: `Bearer ${this.apiKey}` },
69
- signal: AbortSignal.timeout(5000),
70
- });
71
- return res.ok ? 'available' : 'error';
72
- } catch {
73
- return 'unavailable';
74
- }
75
- }
76
-
77
- async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResult> {
78
- if (!this.apiKey) {
79
- throw new Error('OpenAI API key not configured. Set it in Settings → Providers.');
80
- }
81
-
82
- const model = options?.model || this.defaultModel;
83
- const start = Date.now();
84
-
85
- const body: Record<string, unknown> = {
86
- model,
87
- messages: messages.map(m => ({ role: m.role, content: m.content })),
88
- temperature: options?.temperature ?? 0.7,
89
- max_tokens: options?.maxTokens ?? 2048,
90
- };
91
-
92
- const res = await fetch(`${this.baseUrl}/chat/completions`, {
93
- method: 'POST',
94
- headers: {
95
- Authorization: `Bearer ${this.apiKey}`,
96
- 'Content-Type': 'application/json',
97
- },
98
- body: JSON.stringify(body),
99
- signal: options?.signal ?? AbortSignal.timeout(60000),
100
- });
101
-
102
- if (!res.ok) {
103
- const errorBody = await res.text();
104
- throw new Error(`OpenAI API error ${res.status}: ${errorBody}`);
105
- }
106
-
107
- const data = await res.json();
108
- const latencyMs = Date.now() - start;
109
-
110
- const text = data.choices?.[0]?.message?.content || '(sin respuesta)';
111
- const tokensUsed =
112
- (data.usage?.prompt_tokens || 0) + (data.usage?.completion_tokens || 0);
113
-
114
- return { text, model, latencyMs, tokensUsed };
115
- }
116
-
117
- /** Stream chat completion — yields text chunks */
118
- async *chatStream(
119
- messages: ChatMessage[],
120
- options?: ChatOptions,
121
- ): AsyncGenerator<string, void, unknown> {
122
- if (!this.apiKey) {
123
- throw new Error('OpenAI API key not configured.');
124
- }
125
-
126
- const model = options?.model || this.defaultModel;
127
-
128
- const body: Record<string, unknown> = {
129
- model,
130
- messages: messages.map(m => ({ role: m.role, content: m.content })),
131
- temperature: options?.temperature ?? 0.7,
132
- max_tokens: options?.maxTokens ?? 2048,
133
- stream: true,
134
- };
135
-
136
- const res = await fetch(`${this.baseUrl}/chat/completions`, {
137
- method: 'POST',
138
- headers: {
139
- Authorization: `Bearer ${this.apiKey}`,
140
- 'Content-Type': 'application/json',
141
- },
142
- body: JSON.stringify(body),
143
- signal: options?.signal ?? AbortSignal.timeout(120000),
144
- });
145
-
146
- if (!res.ok) {
147
- const errorBody = await res.text();
148
- throw new Error(`OpenAI stream error ${res.status}: ${errorBody}`);
149
- }
150
-
151
- const reader = res.body?.getReader();
152
- if (!reader) return;
153
-
154
- const decoder = new TextDecoder();
155
- let buffer = '';
156
-
157
- while (true) {
158
- const { done, value } = await reader.read();
159
- if (done) break;
160
-
161
- buffer += decoder.decode(value, { stream: true });
162
- const lines = buffer.split('\n');
163
- buffer = lines.pop() || '';
164
-
165
- for (const line of lines) {
166
- if (!line.startsWith('data: ')) continue;
167
- const data = line.slice(6).trim();
168
- if (data === '[DONE]') return;
169
-
170
- try {
171
- const chunk = JSON.parse(data);
172
- const delta = chunk.choices?.[0]?.delta?.content;
173
- if (delta) yield delta;
174
- } catch { /* skip malformed */ }
175
- }
176
- }
177
- }
178
- }
package/src/crypto.ts DELETED
@@ -1,54 +0,0 @@
1
- /**
2
- * Utility functions for decrypting the Zero-Trust encrypted Swarm Bus messages
3
- */
4
-
5
- /**
6
- * Decrypts a base64 encoded payload that contains [12-byte Nonce][Ciphertext]
7
- * using AES-256-GCM and the provided Swarm Key
8
- *
9
- * @param base64Payload The encrypted base64 string from Redis
10
- * @param base64Key The base64 string representing the 32-byte Swarm Key
11
- * @returns The parsed JSON object of the event
12
- */
13
- export async function decryptEvent(base64Payload: string, base64Key: string): Promise<any> {
14
- try {
15
- // 1. Decode base64 key
16
- const keyBuffer = Uint8Array.from(atob(base64Key), c => c.charCodeAt(0));
17
-
18
- // 2. Decode base64 payload
19
- const payloadBuffer = Uint8Array.from(atob(base64Payload), c => c.charCodeAt(0));
20
-
21
- if (payloadBuffer.byteLength < 12) {
22
- throw new Error("Payload too short to contain a valid nonce");
23
- }
24
-
25
- // 3. Extract Nonce (first 12 bytes) and Ciphertext (the rest)
26
- const nonce = payloadBuffer.slice(0, 12);
27
- const ciphertext = payloadBuffer.slice(12);
28
-
29
- // 4. Import the key for subtle crypto
30
- const cryptoKey = await crypto.subtle.importKey(
31
- "raw",
32
- keyBuffer,
33
- { name: "AES-GCM" },
34
- false,
35
- ["decrypt"]
36
- );
37
-
38
- // 5. Decrypt
39
- const decryptedBuffer = await crypto.subtle.decrypt(
40
- { name: "AES-GCM", iv: nonce },
41
- cryptoKey,
42
- ciphertext
43
- );
44
-
45
- // 6. Parse JSON
46
- const decoder = new TextDecoder();
47
- const jsonString = decoder.decode(decryptedBuffer);
48
-
49
- return JSON.parse(jsonString);
50
- } catch (e) {
51
- console.error("[Crypto Bridge] Failed to decrypt secure event:", e);
52
- throw e;
53
- }
54
- }
package/src/kernel.ts DELETED
@@ -1,376 +0,0 @@
1
- import { io, Socket } from 'socket.io-client';
2
- import { inferenceRouter, TaskType, InferenceBackend } from './ai/services/InferenceRouter';
3
- import { createSafeEvent } from '@decido/sdk';
4
-
5
- // Tauri modules are loaded dynamically to avoid breaking browser-only hosts
6
- // (e.g. theme-playground) where @tauri-apps/* is stubbed out by Vite.
7
- async function getTauriCore() { return import('@tauri-apps/api/core'); }
8
- async function getTauriEvent() { return import('@tauri-apps/api/event'); }
9
-
10
- export interface LogEntry {
11
- id: string;
12
- timestamp: number;
13
- type: 'ipc_out' | 'ipc_in' | 'info' | 'error';
14
- channel: string;
15
- payload?: any;
16
- duration?: number;
17
- }
18
-
19
- // -----------------------------------------------------
20
- // TRANSPORT ADAPTERS (Tauri vs WebSocket)
21
- // -----------------------------------------------------
22
- export interface KernelTransport {
23
- name: string;
24
- isConnectedToCloud: boolean;
25
- connectToSwarm(tenantId: string, url: string, token?: string): void;
26
- execute<T>(tenantId: string, cmd: string, args?: Record<string, unknown>): Promise<T>;
27
- notify(title: string, body: string): Promise<void>;
28
- vibrate(pattern: any): Promise<void>;
29
- getEventHistory(tenantId: string, limit: number): Promise<any[]>;
30
- listenEvents(onEvent: (payload: { type: string, channel: string, payload: any }) => void): () => void;
31
- }
32
-
33
- class TauriTransport implements KernelTransport {
34
- name = "Anillo 0/1 (Tauri IPC)";
35
- isConnectedToCloud = false;
36
- private unlistenFns: (() => void)[] = [];
37
-
38
- connectToSwarm(tenantId: string, url: string, token?: string) {
39
- // En Anillo 0, Tauri (Rust) se encarga de la conexión WebSocket en background
40
- // Podríamos enviar un invoke('connect_swarm') aquí si fuera necesario
41
- console.log(`[Kernel/Tauri] Conexión nativa delegada a Core-Rust para tenant: ${tenantId}`);
42
- }
43
-
44
- async execute<T>(tenantId: string, cmd: string, args?: Record<string, unknown>): Promise<T> {
45
- const { invoke } = await getTauriCore();
46
- return invoke<T>(cmd, { tenantId, ...args });
47
- }
48
-
49
- async notify(title: string, body: string): Promise<void> {
50
- try {
51
- const { sendNotification } = await import('@tauri-apps/plugin-notification');
52
- await sendNotification({ title, body });
53
- } catch (e) {
54
- console.log('[Kernel/Tauri] Notification plugin not available.');
55
- }
56
- }
57
-
58
- async vibrate(pattern: any): Promise<void> {
59
- try {
60
- const { vibrate: nativeVibrate } = await import('@tauri-apps/plugin-haptics');
61
- await nativeVibrate(pattern);
62
- } catch (e) {
63
- console.log('[Kernel/Tauri] Haptics not available on this platform.');
64
- }
65
- }
66
-
67
- async getEventHistory(tenantId: string, limit: number): Promise<any[]> {
68
- return this.execute<any[]>(tenantId, 'get_stream_history', { limit });
69
- }
70
-
71
- listenEvents(onEvent: (payload: { type: string, channel: string, payload: any }) => void): () => void {
72
- let unlistenFn: (() => void) | null = null;
73
- getTauriEvent().then(({ listen }) => {
74
- listen('orchestrator-event', (event: any) => {
75
- onEvent({ type: 'ipc_in', channel: 'orchestrator-event', payload: event.payload });
76
- }).then(fn => {
77
- unlistenFn = fn;
78
- this.unlistenFns.push(fn);
79
- });
80
- });
81
-
82
- return () => {
83
- if (unlistenFn) {
84
- unlistenFn();
85
- this.unlistenFns = this.unlistenFns.filter(f => f !== unlistenFn);
86
- }
87
- };
88
- }
89
- }
90
-
91
- class WebSocketTransport implements KernelTransport {
92
- name = "Satélite Web (WebSocket)";
93
- isConnectedToCloud = false;
94
- private sockets: Record<string, Socket> = {};
95
- private eventListeners: Set<(payload: { type: string, channel: string, payload: any }) => void> = new Set();
96
-
97
- connectToSwarm(tenantId: string, url: string, token?: string) {
98
- if (this.sockets[tenantId]) return;
99
- const socket = io(url, { auth: token ? { token } : undefined });
100
-
101
- socket.on('connect', () => {
102
- this.isConnectedToCloud = true;
103
- console.log(`✅ [Kernel/WS] Satélite conectado al Enjambre del Tenant: ${tenantId}`);
104
- });
105
-
106
- socket.on('orchestrator-event', (eventPayload) => {
107
- this.eventListeners.forEach(cb => cb({ type: 'ipc_in', channel: 'orchestrator-event', payload: { tenantId, ...eventPayload } }));
108
- });
109
-
110
- socket.on('rpc_broadcast', (eventPayload) => {
111
- console.log(`[Kernel] 🌐 Broadcast Recibido [${tenantId}]:`, eventPayload);
112
- this.eventListeners.forEach(cb => cb({ type: 'ipc_in', channel: 'rpc_broadcast', payload: { tenantId, ...eventPayload } }));
113
- });
114
-
115
- this.sockets[tenantId] = socket;
116
- }
117
-
118
- async execute<T>(tenantId: string, cmd: string, args?: Record<string, unknown>): Promise<T> {
119
- const socket = this.sockets[tenantId];
120
- if (socket) {
121
- return new Promise((resolve, reject) => {
122
- socket.emit('rpc_call', { cmd, args }, (response: any) => {
123
- if (response.error) reject(new Error(response.error));
124
- else resolve(response.data as T);
125
- });
126
- });
127
- }
128
- console.warn(`[Kernel Mock] Executing ${cmd} for tenant ${tenantId}`, args);
129
- return {} as T;
130
- }
131
-
132
- async notify(title: string, body: string): Promise<void> {
133
- console.log(`[Notification Mock] ${title}: ${body}`);
134
- }
135
-
136
- async vibrate(pattern: any): Promise<void> {
137
- if (typeof navigator !== 'undefined' && navigator.vibrate) {
138
- navigator.vibrate(pattern === 'heavy' ? 200 : 50);
139
- }
140
- }
141
-
142
- async getEventHistory(tenantId: string, limit: number): Promise<any[]> {
143
- console.warn('[Kernel Mock] Cannot fetch stream history in Web Sandbox.');
144
- return [];
145
- }
146
-
147
- listenEvents(onEvent: (payload: { type: string, channel: string, payload: any }) => void): () => void {
148
- this.eventListeners.add(onEvent);
149
- return () => this.eventListeners.delete(onEvent);
150
- }
151
- }
152
-
153
- // -----------------------------------------------------
154
- // HIGH LEVEL KERNEL API
155
- // -----------------------------------------------------
156
- class DecidoKernel {
157
- private logs: LogEntry[] = [];
158
- private logListeners: Set<(logs: LogEntry[]) => void> = new Set();
159
- private universalListeners: Set<(event: unknown) => void> = new Set();
160
-
161
- private transport: KernelTransport;
162
- private rateLimits: Map<string, { calls: number, resetAt: number }> = new Map();
163
-
164
- private checkRateLimit(cmd: string): boolean {
165
- const now = Date.now();
166
- const limitWin = 1000; // 1 second window
167
- const maxCalls = 50; // 50 calls per second per command
168
-
169
- let tracker = this.rateLimits.get(cmd);
170
- if (!tracker || tracker.resetAt < now) {
171
- tracker = { calls: 0, resetAt: now + limitWin };
172
- this.rateLimits.set(cmd, tracker);
173
- }
174
-
175
- tracker.calls++;
176
- if (tracker.calls > maxCalls) {
177
- console.warn(`[Kernel Throttle] 🚨 Command ${cmd} blocked (Rate limit: >50 calls/sec). Possible plugin infinite loop.`);
178
- return false;
179
- }
180
- return true;
181
- }
182
-
183
- constructor() {
184
- // Factory detect
185
- const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;
186
- this.transport = isTauri ? new TauriTransport() : new WebSocketTransport();
187
-
188
- console.log(`[Kernel] Entorno Detectado. Operando en Modo ${this.transport.name}.`);
189
-
190
- // Global Event Hook
191
- this.transport.listenEvents((event) => {
192
- if (event.type === 'ipc_in') {
193
- this.addLog({ type: 'ipc_in', channel: event.channel, payload: event.payload });
194
- this.notifyUniversalListeners(event.payload.payload || event.payload);
195
- }
196
- });
197
- }
198
-
199
- public get isConnectedToCloud() {
200
- return this.transport.isConnectedToCloud;
201
- }
202
-
203
- public connectToSwarm(tenantId: string, url: string, token?: string) {
204
- this.transport.connectToSwarm(tenantId, url, token);
205
- }
206
-
207
- // Validador Zod y Notificador Universal
208
- private notifyUniversalListeners(rawPayload: unknown) {
209
- const parsed = createSafeEvent(rawPayload);
210
-
211
- if (!parsed.success) {
212
- console.error('[Kernel] 🚨 Inbound Mesh Event Failed Contract Validation:', parsed.error.format());
213
- this.universalListeners.forEach(cb => cb({
214
- type: 'system_alert',
215
- source: 'kernel-bridge',
216
- payload: {
217
- level: 'error',
218
- title: 'Contract Violation',
219
- message: 'A received event did not match the strict schema. Ignored.'
220
- }
221
- }));
222
- return;
223
- }
224
- this.universalListeners.forEach(cb => cb(parsed.data));
225
- }
226
-
227
- injectEvent(payload: unknown) {
228
- this.notifyUniversalListeners(payload);
229
- }
230
-
231
- private addLog(entry: Omit<LogEntry, 'id' | 'timestamp'>) {
232
- const log: LogEntry = {
233
- id: typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2, 9),
234
- timestamp: Date.now(),
235
- ...entry
236
- };
237
- this.logs = [log, ...this.logs].slice(0, 500);
238
- this.logListeners.forEach(listener => listener(this.logs));
239
- }
240
-
241
- onLogsChange(callback: (logs: LogEntry[]) => void): () => void {
242
- this.logListeners.add(callback);
243
- callback(this.logs);
244
- return () => this.logListeners.delete(callback);
245
- }
246
-
247
- async execute<T>(tenantIdOrCmd: string, cmdOrArgs?: string | Record<string, unknown>, argsOpts?: Record<string, unknown>): Promise<T> {
248
- let tenantId = 'system';
249
- let cmd = tenantIdOrCmd;
250
- let args = cmdOrArgs as Record<string, unknown> | undefined;
251
-
252
- if (typeof cmdOrArgs === 'string') {
253
- tenantId = tenantIdOrCmd;
254
- cmd = cmdOrArgs;
255
- args = argsOpts;
256
- }
257
-
258
- if (!this.checkRateLimit(cmd)) {
259
- throw new Error(`[Kernel Throttle] Request dropped for ${cmd} (Rate limit exceeded)`);
260
- }
261
-
262
- const start = performance.now();
263
- try {
264
- const result = await this.transport.execute<T>(tenantId, cmd, args);
265
- this.addLog({ type: 'ipc_out', channel: cmd, payload: { tenantId, ...args }, duration: performance.now() - start });
266
- return result;
267
- } catch (error) {
268
- this.addLog({ type: 'error', channel: cmd, payload: { tenantId, args, error }, duration: performance.now() - start });
269
- throw error;
270
- }
271
- }
272
-
273
- async notify(title: string, body: string): Promise<void> {
274
- return this.transport.notify(title, body);
275
- }
276
-
277
- async vibrate(pattern: 'light' | 'medium' | 'heavy' | 'selection' | 'success' | 'warning' | 'error' = 'medium'): Promise<void> {
278
- return this.transport.vibrate(pattern);
279
- }
280
-
281
- async captureScreen(tenantId: string = 'system'): Promise<string> {
282
- return this.execute<string>(tenantId, 'capture_screen');
283
- }
284
-
285
- async getMachineId(tenantId: string = 'system'): Promise<string> {
286
- return this.execute<string>(tenantId, 'get_machine_id');
287
- }
288
-
289
- async initiateSingularity(tenantId: string = 'system'): Promise<any[]> {
290
- return this.execute<any[]>(tenantId, 'initiate_singularity');
291
- }
292
-
293
- async broadcastMessage(tenantId: string, message: string): Promise<number> {
294
- return this.execute<number>(tenantId, 'broadcast_message', { message });
295
- }
296
-
297
- async fetchMemories(tenantId: string = 'system', query: string, limit: number = 20): Promise<any[]> {
298
- return this.execute<any[]>(tenantId, 'fetch_memories', { query, limit });
299
- }
300
-
301
- async storeMemory(tenantId: string = 'system', text: string, metadata: Record<string, unknown> = {}): Promise<void> {
302
- return this.execute<void>(tenantId, 'store_memory', { text, metadata });
303
- }
304
-
305
- async deleteMemory(tenantId: string = 'system', id: string): Promise<void> {
306
- return this.execute<void>(tenantId, 'delete_memory', { id });
307
- }
308
-
309
- async reasonWithMLX(base64Image: string, prompt: string, previousError?: string): Promise<any> {
310
- let finalPrompt = prompt;
311
- if (previousError) {
312
- finalPrompt += `\n\nWARNING: Your previous response failed to parse as JSON with the following error: ${previousError}. Fix the JSON syntax and return ONLY a valid JSON object.`;
313
- }
314
- try {
315
- const response = await fetch('http://127.0.0.1:8080/v1/chat/completions', {
316
- method: 'POST',
317
- headers: { 'Content-Type': 'application/json' },
318
- body: JSON.stringify({
319
- model: 'llava',
320
- messages: [{
321
- role: 'user',
322
- content: [
323
- { type: 'text', text: finalPrompt },
324
- { type: 'image_url', image_url: { url: base64Image } }
325
- ]
326
- }],
327
- max_tokens: 150, temperature: 0.1
328
- })
329
- });
330
- const data = await response.json();
331
- let content = data.choices[0].message.content.trim();
332
- if (content.startsWith('```json')) content = content.replace(/```json\n?/, '').replace(/```$/, '').trim();
333
- else if (content.startsWith('```')) content = content.replace(/```\n?/, '').replace(/```$/, '').trim();
334
- return JSON.parse(content);
335
- } catch (e: any) {
336
- console.error('[MLX Bridge] Reasoning failed:', e);
337
- throw e;
338
- }
339
- }
340
-
341
- async executeAction(tenantId: string = 'system', action: any): Promise<any> {
342
- return this.execute(tenantId, 'execute_action', { action });
343
- }
344
-
345
- async runAutonomousStep(tenantId: string = 'system', objective: string): Promise<void> {
346
- const image = await this.captureScreen(tenantId);
347
- let decision = null, retries = 0, lastError = null;
348
- const instruction = `You are an AI OS agent. Based on the screen image, what action should be taken to achieve: "${objective}"? Return ONLY a strict JSON object of the action, no markdown. E.g. {"type": "mouseClick", "x": 100, "y": 200, "button": "left", "count": 1}`;
349
- while (retries <= 2 && !decision) {
350
- try {
351
- decision = await this.reasonWithMLX(image, instruction, lastError || undefined);
352
- } catch (error: any) {
353
- lastError = error.message || String(error);
354
- retries++;
355
- }
356
- }
357
- if (!decision) return;
358
- await this.executeAction(tenantId, decision);
359
- }
360
-
361
- async askAI(prompt: string, taskType: TaskType = 'reasoning' as any, forceProvider?: InferenceBackend): Promise<string> {
362
- const result = await inferenceRouter.route(prompt, { preferredBackend: forceProvider || 'auto' });
363
- return result.text;
364
- }
365
-
366
- onEvent(callback: (eventData: any) => void): () => void {
367
- this.universalListeners.add(callback);
368
- return () => this.universalListeners.delete(callback);
369
- }
370
-
371
- async getEventHistory(tenantId: string, limit: number = 50): Promise<any[]> {
372
- return this.transport.getEventHistory(tenantId, limit);
373
- }
374
- }
375
-
376
- export const kernel = new DecidoKernel();