@takimcizgisi/ajan 0.5.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/LICENSE +227 -0
- package/README.md +324 -0
- package/config/models.json +50 -0
- package/out/ajan-engine.js +8306 -0
- package/package.json +84 -0
- package/src/client/client.ts +289 -0
- package/src/config/configManager.ts +300 -0
- package/src/config/types.ts +139 -0
- package/src/core/agent.ts +379 -0
- package/src/daemon/gpu.ts +54 -0
- package/src/daemon/main.ts +87 -0
- package/src/daemon/runner.ts +261 -0
- package/src/daemon/server.ts +231 -0
- package/src/engine/llama.ts +329 -0
- package/src/engine/modelManager.ts +84 -0
- package/src/index.ts +63 -0
- package/src/protocol/channel.ts +151 -0
- package/src/protocol/messages.ts +138 -0
- package/src/service.ts +410 -0
- package/src/tools/data.ts +101 -0
- package/src/tools/edit.ts +54 -0
- package/src/tools/file.ts +261 -0
- package/src/tools/filesys.ts +70 -0
- package/src/tools/memory.ts +76 -0
- package/src/tools/patch.ts +138 -0
- package/src/tools/playground.ts +130 -0
- package/src/tools/registry.ts +79 -0
- package/src/tools/taskComplete.ts +34 -0
- package/src/tools/terminal.ts +138 -0
- package/src/tools/utils.ts +63 -0
- package/src/tools/web.ts +204 -0
- package/src/utils/paths.ts +80 -0
- package/src/utils/projectContext.ts +23 -0
- package/src/utils/sessions.ts +91 -0
- package/src/version.ts +4 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AJAN IPC protokolü — sözleşmenin tek kaynağı.
|
|
3
|
+
*
|
|
4
|
+
* Hem daemon (`src/daemon`) hem istemci (`src/client`) buradan içe aktarır.
|
|
5
|
+
* Bu dosya çalışma zamanında hiçbir şey yapmaz: yalnızca tipler ve iki küçük
|
|
6
|
+
* doğrulayıcı. Böylece iki taraf aynı sözleşmeyi görmeye zorlanır.
|
|
7
|
+
*
|
|
8
|
+
* Çerçeveleme: JSON Lines. Her mesaj tek satırlık geçerli JSON'dur; akış
|
|
9
|
+
* (token) olayları bu yüzden satır ayrımıyla taşınabilir.
|
|
10
|
+
*/
|
|
11
|
+
import type { StoredSession } from "../utils/sessions.js";
|
|
12
|
+
import type { AjanConfig, ModelDefinition, ReasoningMode } from "../config/types.js";
|
|
13
|
+
|
|
14
|
+
export const PROTOCOL_VERSION = 1;
|
|
15
|
+
|
|
16
|
+
// ── İstekler ──
|
|
17
|
+
|
|
18
|
+
export type Request =
|
|
19
|
+
| { type: "ping" }
|
|
20
|
+
| { type: "status" }
|
|
21
|
+
| { type: "set-config"; patch: Record<string, unknown> }
|
|
22
|
+
| { type: "list-models" }
|
|
23
|
+
| { type: "install-model"; id: string }
|
|
24
|
+
| { type: "remove-model"; id: string }
|
|
25
|
+
| { type: "use-model"; id: string }
|
|
26
|
+
| { type: "refresh-models" }
|
|
27
|
+
| { type: "chat"; text: string; modelId?: string }
|
|
28
|
+
| { type: "abort" }
|
|
29
|
+
| { type: "new-chat" }
|
|
30
|
+
| { type: "get-history" }
|
|
31
|
+
| { type: "list-sessions" }
|
|
32
|
+
| { type: "load-session"; id: string }
|
|
33
|
+
| { type: "save-session"; session: StoredSession }
|
|
34
|
+
| { type: "delete-session"; id: string }
|
|
35
|
+
| { type: "shutdown" };
|
|
36
|
+
|
|
37
|
+
export type RequestType = Request["type"];
|
|
38
|
+
|
|
39
|
+
// ── Yanıtlar ──
|
|
40
|
+
|
|
41
|
+
export type ModelEntry = ModelDefinition & { path: string; installed: boolean; active: boolean };
|
|
42
|
+
export type SessionSummary = { id: string; title: string; updatedAt: string };
|
|
43
|
+
|
|
44
|
+
export type GpuReport = {
|
|
45
|
+
/** Gerçekte yüklenen arka uç: "cpu" | "cuda" | "vulkan" | "metal" */
|
|
46
|
+
active: string;
|
|
47
|
+
/** GPU'ya katman aktarımı açık mı */
|
|
48
|
+
offload: boolean;
|
|
49
|
+
/** Kullanılan GPU belleği (MB) */
|
|
50
|
+
vramMb: number;
|
|
51
|
+
/** GPU'ya yüklenen katman sayısı (0 = henüz yüklenmedi) */
|
|
52
|
+
layers: number;
|
|
53
|
+
/** Bu makinede sürücüsü bulunan arka uçlar */
|
|
54
|
+
supported: string[];
|
|
55
|
+
/** config.gpu değeri */
|
|
56
|
+
preferred: string;
|
|
57
|
+
/** İstenen arka uç kullanılamayıp başka birine düşüldü mü */
|
|
58
|
+
fallback: boolean;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type StatusReport = {
|
|
62
|
+
protocol: number;
|
|
63
|
+
version: string;
|
|
64
|
+
modelId: string;
|
|
65
|
+
modelName: string;
|
|
66
|
+
installed: boolean;
|
|
67
|
+
loading: boolean;
|
|
68
|
+
busy: boolean;
|
|
69
|
+
/** Ajanın çalışma dizini */
|
|
70
|
+
cwd: string;
|
|
71
|
+
gpu: GpuReport;
|
|
72
|
+
context: { usedTokens: number; contextSize: number } | null;
|
|
73
|
+
reasoning: { mode: ReasoningMode; enabled: boolean } | null;
|
|
74
|
+
config: AjanConfig;
|
|
75
|
+
/** Bağlı istemci sayısı */
|
|
76
|
+
clients: number;
|
|
77
|
+
uptimeSec: number;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type Result =
|
|
81
|
+
| { type: "pong"; protocol: number; version: string }
|
|
82
|
+
| { type: "status"; status: StatusReport }
|
|
83
|
+
| { type: "config"; config: AjanConfig }
|
|
84
|
+
| { type: "models"; models: ModelEntry[]; error?: string }
|
|
85
|
+
| { type: "model"; id: string; ok: boolean; error?: string }
|
|
86
|
+
| { type: "models-refreshed"; ok: boolean; count: number; error?: string }
|
|
87
|
+
| { type: "history"; messages: HistoryMessage[] }
|
|
88
|
+
| { type: "sessions"; sessions: SessionSummary[] }
|
|
89
|
+
| { type: "session"; session: StoredSession | null; ok: boolean; error?: string }
|
|
90
|
+
| { type: "ok" }
|
|
91
|
+
| { type: "shutdown"; ok: true };
|
|
92
|
+
|
|
93
|
+
export type HistoryMessage = { kind: "user" | "assistant" | "thought" | "tool"; text: string; name?: string };
|
|
94
|
+
|
|
95
|
+
/** İstek zarfı */
|
|
96
|
+
export type RequestFrame = { id: number; req: Request };
|
|
97
|
+
/** Yanıt zarfı */
|
|
98
|
+
export type ResponseFrame = { id: number; ok: true; result: Result } | { id: number; ok: false; error: string };
|
|
99
|
+
|
|
100
|
+
// ── Olaylar (motor → istemci, `id` taşımaz) ──
|
|
101
|
+
|
|
102
|
+
export type EventFrame =
|
|
103
|
+
| { event: "load-progress"; pct: number }
|
|
104
|
+
| { event: "load-complete" }
|
|
105
|
+
| { event: "step-start"; step: number; maxSteps: number }
|
|
106
|
+
| { event: "step-end"; step: number }
|
|
107
|
+
| { event: "text-chunk"; text: string }
|
|
108
|
+
| { event: "thinking-chunk"; text: string }
|
|
109
|
+
| { event: "tool-call"; name: string; args: Record<string, unknown> }
|
|
110
|
+
| { event: "tool-result"; name: string; ok: boolean; output: string }
|
|
111
|
+
| { event: "complete"; response: string; steps: number }
|
|
112
|
+
| { event: "error"; message: string }
|
|
113
|
+
| { event: "log"; level: "debug" | "info" | "warn" | "error"; message: string };
|
|
114
|
+
|
|
115
|
+
export type EventName = EventFrame["event"];
|
|
116
|
+
|
|
117
|
+
/** İstemciye giden her şey */
|
|
118
|
+
export type ServerFrame = ResponseFrame | EventFrame;
|
|
119
|
+
|
|
120
|
+
// ── Doğrulayıcılar ──
|
|
121
|
+
|
|
122
|
+
export function isRequestFrame(value: unknown): value is RequestFrame {
|
|
123
|
+
if (!value || typeof value !== "object") return false;
|
|
124
|
+
const f = value as { id?: unknown; req?: { type?: unknown } };
|
|
125
|
+
return typeof f.id === "number" && !!f.req && typeof f.req === "object" && typeof f.req.type === "string";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function isEventFrame(value: unknown): value is EventFrame {
|
|
129
|
+
if (!value || typeof value !== "object") return false;
|
|
130
|
+
const f = value as { event?: unknown };
|
|
131
|
+
return typeof f.event === "string";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function isResponseFrame(value: unknown): value is ResponseFrame {
|
|
135
|
+
if (!value || typeof value !== "object") return false;
|
|
136
|
+
const f = value as { id?: unknown; event?: unknown };
|
|
137
|
+
return typeof f.id === "number" && f.event === undefined;
|
|
138
|
+
}
|
package/src/service.ts
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import type { ChatHistoryItem } from "node-llama-cpp";
|
|
2
|
+
import { AjanAgent, type AgentEvents } from "./core/agent.js";
|
|
3
|
+
import {
|
|
4
|
+
loadConfig, saveConfig, getModelById, isModelInstalled,
|
|
5
|
+
listInstalledModels as listInstalledFromDisk, removeModelFile,
|
|
6
|
+
resolveModelFilePath, refreshRemoteModels, getModelsRegistry
|
|
7
|
+
} from "./config/configManager.js";
|
|
8
|
+
import {
|
|
9
|
+
saveSession as saveSessionFile, loadSession as loadSessionFile,
|
|
10
|
+
listSessions as listSessionsFromDisk, generateSessionId, deleteSession as deleteSessionFile
|
|
11
|
+
} from "./utils/sessions.js";
|
|
12
|
+
import type { StoredSession, StoredMessage } from "./utils/sessions.js";
|
|
13
|
+
import type { AjanConfig, ModelDefinition, ReasoningMode } from "./config/types.js";
|
|
14
|
+
|
|
15
|
+
export type ServiceEvents = AgentEvents & {
|
|
16
|
+
onModelLoadProgress?: (pct: number) => void;
|
|
17
|
+
onModelLoadComplete?: () => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** İstemciye raporlanacak GPU durumu */
|
|
21
|
+
export type GpuStatus = {
|
|
22
|
+
/** Gerçekte yüklenen arka uç; null ise model henüz yüklenmemiş */
|
|
23
|
+
active: { backend: string; offload: boolean } | null;
|
|
24
|
+
/** Kullanılabilen GPU belleği (MB) */
|
|
25
|
+
vramMb: number;
|
|
26
|
+
/** GPU'ya yüklenen katman sayısı (0 = henüz yüklenmedi) */
|
|
27
|
+
layers: number;
|
|
28
|
+
/** Bu makinede sürücüsü bulunan arka uçlar */
|
|
29
|
+
supported: string[];
|
|
30
|
+
/** config.gpu değeri */
|
|
31
|
+
preferred: string;
|
|
32
|
+
/** İstenen arka uç kullanılamayıp başka birine düşüldü mü */
|
|
33
|
+
fallback: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type ModelWithStatus = {
|
|
37
|
+
model: ModelDefinition;
|
|
38
|
+
path: string;
|
|
39
|
+
installed: boolean;
|
|
40
|
+
active: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export class AjanService {
|
|
44
|
+
private agent: AjanAgent | null = null;
|
|
45
|
+
private config: AjanConfig;
|
|
46
|
+
private events: ServiceEvents;
|
|
47
|
+
private readonly cwd: string;
|
|
48
|
+
private _active = false;
|
|
49
|
+
private _currentAbort: AbortController | null = null;
|
|
50
|
+
|
|
51
|
+
constructor(events: ServiceEvents = {}, cwd?: string) {
|
|
52
|
+
this.config = loadConfig();
|
|
53
|
+
this.events = events;
|
|
54
|
+
this.cwd = cwd ?? process.cwd();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Agent Lifecycle ──
|
|
58
|
+
|
|
59
|
+
private ensureAgent(): void {
|
|
60
|
+
if (this.agent) return;
|
|
61
|
+
this.agent = new AjanAgent({
|
|
62
|
+
config: this.config,
|
|
63
|
+
cwd: this.cwd,
|
|
64
|
+
events: {
|
|
65
|
+
onTextChunk: this.events.onTextChunk,
|
|
66
|
+
onThinkingChunk: this.events.onThinkingChunk,
|
|
67
|
+
onToolCall: this.events.onToolCall,
|
|
68
|
+
onToolResult: this.events.onToolResult,
|
|
69
|
+
onComplete: this.events.onComplete,
|
|
70
|
+
onStepStart: this.events.onStepStart,
|
|
71
|
+
onStepEnd: this.events.onStepEnd,
|
|
72
|
+
onContextTrimmed: this.events.onContextTrimmed,
|
|
73
|
+
onError: this.events.onError
|
|
74
|
+
},
|
|
75
|
+
onLoadProgress: this.events.onModelLoadProgress,
|
|
76
|
+
onLoadComplete: this.events.onModelLoadComplete
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Event'leri güncelle ve aktif ajana anında uygula.
|
|
82
|
+
*
|
|
83
|
+
* Daemon her sohbet isteğinde farklı bir istemciye özel yayın yapar; bu
|
|
84
|
+
* yüzden yalnızca saklamak yeterli değil, ajanın kendi event'leri de
|
|
85
|
+
* güncellenmeli. `events` verilmezse mevcut yayın korunur.
|
|
86
|
+
*/
|
|
87
|
+
updateEvents(events: ServiceEvents): void {
|
|
88
|
+
if (events) this.events = { ...this.events, ...events };
|
|
89
|
+
this.agent?.setEvents(this.toAgentEvents());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** ServiceEvents → AjanAgent olay eşleyicisi (tek doğruluk kaynağı) */
|
|
93
|
+
private toAgentEvents(): AgentEvents {
|
|
94
|
+
const e = this.events;
|
|
95
|
+
return {
|
|
96
|
+
onTextChunk: e.onTextChunk,
|
|
97
|
+
onThinkingChunk: e.onThinkingChunk,
|
|
98
|
+
onToolCall: e.onToolCall,
|
|
99
|
+
onToolResult: e.onToolResult,
|
|
100
|
+
onComplete: e.onComplete,
|
|
101
|
+
onStepStart: e.onStepStart,
|
|
102
|
+
onStepEnd: e.onStepEnd,
|
|
103
|
+
onContextTrimmed: e.onContextTrimmed,
|
|
104
|
+
onError: e.onError
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Prompt ──
|
|
109
|
+
|
|
110
|
+
async sendMessage(message: string, modelId?: string): Promise<{
|
|
111
|
+
ok: boolean; response?: string; steps?: number;
|
|
112
|
+
completed?: boolean; error?: string;
|
|
113
|
+
}> {
|
|
114
|
+
if (this._active) return { ok: false, error: "Meşgul" };
|
|
115
|
+
|
|
116
|
+
// Model değişikliği varsa
|
|
117
|
+
if (modelId && modelId !== this.config.modelId) {
|
|
118
|
+
this.config.modelId = modelId;
|
|
119
|
+
saveConfig(this.config);
|
|
120
|
+
await this.recreateAgent();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Model indirilmemişse ajan kurma: aksi halde llama'nın ENOENT'si
|
|
124
|
+
// kullanıcıya ham dosya yolu olarak sızıyor.
|
|
125
|
+
const model = getModelById(this.config.modelId);
|
|
126
|
+
if (!model) {
|
|
127
|
+
return { ok: false, error: `Model tanımlı değil: ${this.config.modelId}. Model listesini yenile ve bir model seç.` };
|
|
128
|
+
}
|
|
129
|
+
if (!isModelInstalled(model)) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
error: `"${model.name}" henüz indirilmedi.\n` + `ajan models install ${model.id} → ${resolveModelFilePath(model)}`
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.ensureAgent();
|
|
137
|
+
if (!this.agent) return { ok: false, error: "Agent başlatılamadı" };
|
|
138
|
+
|
|
139
|
+
this._active = true;
|
|
140
|
+
const controller = new AbortController();
|
|
141
|
+
this._currentAbort = controller;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const result = await this.agent.promptTask(message, { signal: controller.signal });
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
response: result.response,
|
|
148
|
+
steps: result.steps,
|
|
149
|
+
completed: result.completed
|
|
150
|
+
};
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if (controller.signal.aborted) return { ok: false, error: "İptal edildi" };
|
|
153
|
+
return { ok: false, error: (err as Error).message };
|
|
154
|
+
} finally {
|
|
155
|
+
if (this._currentAbort === controller) this._currentAbort = null;
|
|
156
|
+
this._active = false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
abort(): void {
|
|
161
|
+
this._currentAbort?.abort();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async reset(): Promise<void> {
|
|
165
|
+
this.agent?.resetConversation();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── History ──
|
|
169
|
+
|
|
170
|
+
getChatHistory(): ChatHistoryItem[] {
|
|
171
|
+
return this.agent?.getChatHistory() ?? [];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
setChatHistory(history: ChatHistoryItem[]): void {
|
|
175
|
+
this.agent?.setChatHistory(history);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
getContextStats(): { usedTokens: number; contextSize: number } | null {
|
|
179
|
+
return this.agent?.getContextStats() ?? null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Config ──
|
|
183
|
+
|
|
184
|
+
getConfig(): AjanConfig {
|
|
185
|
+
return { ...this.config };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async setConfig(patch: Record<string, unknown>): Promise<void> {
|
|
189
|
+
const merged = { ...this.config, ...patch } as AjanConfig;
|
|
190
|
+
if (patch.session && typeof patch.session === "object") {
|
|
191
|
+
merged.session = { ...this.config.session, ...(patch.session as Record<string, unknown>) } as AjanConfig["session"];
|
|
192
|
+
}
|
|
193
|
+
saveConfig(merged);
|
|
194
|
+
this.config = merged;
|
|
195
|
+
await this.recreateAgent();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Projects ──
|
|
199
|
+
|
|
200
|
+
listProjects(): { path: string; active: boolean }[] {
|
|
201
|
+
const projects = this.config.projects ?? [];
|
|
202
|
+
return projects.map(path => ({ path, active: path === this.config.activeProject }));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async addProject(path: string): Promise<{ ok: boolean; error?: string }> {
|
|
206
|
+
const projects = this.config.projects ?? [];
|
|
207
|
+
if (projects.includes(path)) return { ok: true };
|
|
208
|
+
this.config.projects = [...projects, path];
|
|
209
|
+
saveConfig(this.config);
|
|
210
|
+
return { ok: true };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async removeProject(path: string): Promise<{ ok: boolean; error?: string }> {
|
|
214
|
+
const projects = this.config.projects ?? [];
|
|
215
|
+
this.config.projects = projects.filter(p => p !== path);
|
|
216
|
+
if (this.config.activeProject === path) this.config.activeProject = undefined;
|
|
217
|
+
saveConfig(this.config);
|
|
218
|
+
return { ok: true };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async setActiveProject(path: string | null): Promise<{ ok: boolean; error?: string }> {
|
|
222
|
+
if (path === null) {
|
|
223
|
+
this.config.activeProject = undefined;
|
|
224
|
+
saveConfig(this.config);
|
|
225
|
+
return { ok: true };
|
|
226
|
+
}
|
|
227
|
+
const projects = this.config.projects ?? [];
|
|
228
|
+
if (!projects.includes(path)) return { ok: false, error: `Proje kayıtlı değil: ${path}` };
|
|
229
|
+
this.config.activeProject = path;
|
|
230
|
+
saveConfig(this.config);
|
|
231
|
+
return { ok: true };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Models ──
|
|
235
|
+
|
|
236
|
+
getModelInfo(): { modelId: string; name: string; installed: boolean } | null {
|
|
237
|
+
const model = getModelById(this.config.modelId);
|
|
238
|
+
if (!model) return null;
|
|
239
|
+
return {
|
|
240
|
+
modelId: model.id,
|
|
241
|
+
name: model.name,
|
|
242
|
+
installed: isModelInstalled(model)
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Motorun gerçekten kullandığı arka uç + bu makinede bulunan seçenekler.
|
|
248
|
+
* `active` null ise model henüz yüklenmemiştir; `options` yalnızca kurulu
|
|
249
|
+
* build'de native paketi bulunan arka uçları içerir.
|
|
250
|
+
*/
|
|
251
|
+
async getGpuStatus(): Promise<GpuStatus> {
|
|
252
|
+
const active = this.agent?.activeGpu ?? null;
|
|
253
|
+
const supported = await this.detectGpu();
|
|
254
|
+
const preferred = this.config.gpu ?? "auto";
|
|
255
|
+
// İstenen arka uç ile gerçekte yüklenen farklıysa geri düşüş yapılmıştır.
|
|
256
|
+
// "auto" için fark normaldir: node-llama-cpp kendi sırasını uygular.
|
|
257
|
+
const fallback = active != null && preferred !== "auto" && active.backend !== preferred;
|
|
258
|
+
return {
|
|
259
|
+
active: active ? { backend: active.backend, offload: active.offload } : null,
|
|
260
|
+
vramMb: await this.agent?.gpuVramMb().catch(() => 0) ?? 0,
|
|
261
|
+
layers: this.agent?.gpuLayersUsed ?? 0,
|
|
262
|
+
supported,
|
|
263
|
+
preferred,
|
|
264
|
+
fallback
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Seçili modelin düşünme modu (katalog + kullanıcı geçersiz kılması) */
|
|
269
|
+
getReasoningInfo(): { mode: ReasoningMode; enabled: boolean } | null {
|
|
270
|
+
return this.agent?.reasoningInfo ?? null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
listModels(): ModelWithStatus[] {
|
|
274
|
+
const installed = listInstalledFromDisk();
|
|
275
|
+
return installed.map(item => ({
|
|
276
|
+
model: item.model,
|
|
277
|
+
path: item.path,
|
|
278
|
+
installed: item.installed,
|
|
279
|
+
active: item.model.id === this.config.modelId
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async installModel(modelId: string): Promise<{ ok: boolean; error?: string }> {
|
|
284
|
+
const model = getModelById(modelId);
|
|
285
|
+
if (!model) return { ok: false, error: `Model bulunamadı: ${modelId}` };
|
|
286
|
+
try {
|
|
287
|
+
const { downloadModel } = await import("./engine/modelManager.js");
|
|
288
|
+
await downloadModel(model, {
|
|
289
|
+
onProgress: (progress) => {
|
|
290
|
+
const pct = progress.totalSize > 0
|
|
291
|
+
? Math.round((progress.downloadedSize / progress.totalSize) * 100)
|
|
292
|
+
: progress.percent ?? 0;
|
|
293
|
+
this.events.onModelLoadProgress?.(pct);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
return { ok: true };
|
|
297
|
+
} catch (err) {
|
|
298
|
+
return { ok: false, error: (err as Error).message };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async removeModel(modelId: string): Promise<{ ok: boolean; error?: string }> {
|
|
303
|
+
const model = getModelById(modelId);
|
|
304
|
+
if (!model) return { ok: false, error: `Model bulunamadı: ${modelId}` };
|
|
305
|
+
const removed = removeModelFile(model);
|
|
306
|
+
if (!removed) return { ok: false, error: "Model dosyası bulunamadı" };
|
|
307
|
+
|
|
308
|
+
// Aktif model silindiyse config'i temizle/kurulu bir modele geç
|
|
309
|
+
if (this.config.modelId === modelId) {
|
|
310
|
+
const installed = listInstalledFromDisk().find((m) => m.installed);
|
|
311
|
+
if (installed) {
|
|
312
|
+
this.config.modelId = installed.model.id;
|
|
313
|
+
saveConfig(this.config);
|
|
314
|
+
await this.recreateAgent();
|
|
315
|
+
} else {
|
|
316
|
+
// Kurulu model kalmadı: agent'ı öldür ama servisi ayakta bırak.
|
|
317
|
+
// modelId boş bırakılıp recreateAgent() çağrılırsa servis kalıcı ölüyordu.
|
|
318
|
+
const fallback = getModelsRegistry()[0];
|
|
319
|
+
this.config.modelId = fallback?.id ?? "";
|
|
320
|
+
saveConfig(this.config);
|
|
321
|
+
this._currentAbort?.abort();
|
|
322
|
+
this._currentAbort = null;
|
|
323
|
+
await this.agent?.dispose();
|
|
324
|
+
this.agent = null;
|
|
325
|
+
this._active = false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return { ok: true };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async useModel(modelId: string): Promise<{ ok: boolean; error?: string }> {
|
|
332
|
+
const model = getModelById(modelId);
|
|
333
|
+
if (!model) return { ok: false, error: `Model bulunamadı: ${modelId}` };
|
|
334
|
+
if (!isModelInstalled(model)) return { ok: false, error: `Model kurulu değil: ${modelId}` };
|
|
335
|
+
this.config.modelId = modelId;
|
|
336
|
+
saveConfig(this.config);
|
|
337
|
+
await this.recreateAgent();
|
|
338
|
+
return { ok: true };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async refreshModels(): Promise<{ ok: boolean; count?: number; error?: string }> {
|
|
342
|
+
try {
|
|
343
|
+
const models = await refreshRemoteModels();
|
|
344
|
+
return { ok: true, count: models.length };
|
|
345
|
+
} catch (err) {
|
|
346
|
+
return { ok: false, error: (err as Error).message };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
getModelsRegistry() {
|
|
351
|
+
return getModelsRegistry();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ── Sessions ──
|
|
355
|
+
|
|
356
|
+
async saveSession(session: StoredSession): Promise<void> {
|
|
357
|
+
await saveSessionFile(session);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async loadSession(id: string): Promise<StoredSession | null> {
|
|
361
|
+
const data = await loadSessionFile(id);
|
|
362
|
+
if (data && this.agent) {
|
|
363
|
+
this.agent.setChatHistory(data.history);
|
|
364
|
+
}
|
|
365
|
+
return data;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async listSessions(): Promise<StoredSession[]> {
|
|
369
|
+
return await listSessionsFromDisk();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async deleteSession(id: string): Promise<void> {
|
|
373
|
+
await deleteSessionFile(id);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
generateSessionId(): string {
|
|
377
|
+
return generateSessionId();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── GPU ──
|
|
381
|
+
|
|
382
|
+
async detectGpu(): Promise<string[]> {
|
|
383
|
+
try {
|
|
384
|
+
const { LlamaEngine } = await import("./engine/llama.js");
|
|
385
|
+
return await LlamaEngine.detectSupportedGpus();
|
|
386
|
+
} catch {
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ── Status ──
|
|
392
|
+
|
|
393
|
+
get isActive(): boolean {
|
|
394
|
+
return this._active;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ── Internal ──
|
|
398
|
+
|
|
399
|
+
private async recreateAgent(): Promise<void> {
|
|
400
|
+
await this.agent?.dispose();
|
|
401
|
+
this.agent = null;
|
|
402
|
+
this.ensureAgent();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async dispose(): Promise<void> {
|
|
406
|
+
this._currentAbort?.abort();
|
|
407
|
+
await this.agent?.dispose();
|
|
408
|
+
this.agent = null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { extname, resolve } from "node:path";
|
|
3
|
+
import { load as loadYaml } from "js-yaml";
|
|
4
|
+
import AdmZip from "adm-zip";
|
|
5
|
+
import type { AgentTool } from "../config/types.js";
|
|
6
|
+
import { limitOutput, resolveUserPath } from "./utils.js";
|
|
7
|
+
|
|
8
|
+
function describeStructure(value: unknown, depth = 0): string {
|
|
9
|
+
if (depth > 2) return typeof value;
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
const sample = value.slice(0, 3).map((v) => describeStructure(v, depth + 1));
|
|
12
|
+
return `dizi[${value.length}]<${sample.join(", ")}${value.length > 3 ? ", …" : ""}>`;
|
|
13
|
+
}
|
|
14
|
+
if (value && typeof value === "object") {
|
|
15
|
+
const keys = Object.keys(value as Record<string, unknown>);
|
|
16
|
+
const shown = keys.slice(0, 12).map((k) => `${k}: ${describeStructure((value as Record<string, unknown>)[k], depth + 1)}`);
|
|
17
|
+
return `nesne{${keys.length}}{ ${shown.join(", ")}${keys.length > 12 ? ", …" : ""} }`;
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === "string") return `metin(${value.length})`;
|
|
20
|
+
return String(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const readDataTool: AgentTool = {
|
|
24
|
+
name: "read_data",
|
|
25
|
+
description:
|
|
26
|
+
"JSON veya YAML dosyasını ayrıştırıp özetini ve yapısını döner. Yapılandırma dosyalarını incelemek için idealdir.",
|
|
27
|
+
parameters: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
path: { type: "string", description: "JSON/YAML dosya yolu" }
|
|
31
|
+
},
|
|
32
|
+
required: ["path"]
|
|
33
|
+
},
|
|
34
|
+
handler: async (params: { path?: string }, ctx) => {
|
|
35
|
+
const p = params.path?.trim();
|
|
36
|
+
if (!p) return { ok: false, output: "path zorunludur." };
|
|
37
|
+
const filePath = resolveUserPath(ctx.cwd, p);
|
|
38
|
+
if (!existsSync(filePath)) return { ok: false, output: `Dosya bulunamadı: ${p}` };
|
|
39
|
+
if (!statSync(filePath).isFile()) return { ok: false, output: `Bu bir dosya değil: ${p}` };
|
|
40
|
+
const ext = extname(filePath).toLowerCase();
|
|
41
|
+
let data: unknown;
|
|
42
|
+
try {
|
|
43
|
+
const raw = readFileSync(filePath, "utf8");
|
|
44
|
+
if (ext === ".yaml" || ext === ".yml") {
|
|
45
|
+
data = loadYaml(raw);
|
|
46
|
+
} else if (ext === ".json") {
|
|
47
|
+
data = JSON.parse(raw);
|
|
48
|
+
} else {
|
|
49
|
+
try {
|
|
50
|
+
data = JSON.parse(raw);
|
|
51
|
+
} catch {
|
|
52
|
+
data = loadYaml(raw);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
return { ok: false, output: `Ayrıştırma hatası: ${(err as Error).message}` };
|
|
57
|
+
}
|
|
58
|
+
const pretty = JSON.stringify(data, null, 2);
|
|
59
|
+
const summary = `Dosya: ${filePath}\nYapı: ${describeStructure(data)}\n\n${pretty}`;
|
|
60
|
+
return { ok: true, output: limitOutput(summary, Math.min(ctx.config.maxToolOutput ?? 40_000, 20_000)) };
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const unzipFileTool: AgentTool = {
|
|
65
|
+
name: "unzip_file",
|
|
66
|
+
description: "ZIP arşivini hedef klasöre açar. Hedef verilmezse zip'in yanına açılır.",
|
|
67
|
+
parameters: {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
path: { type: "string", description: "ZIP dosyası yolu" },
|
|
71
|
+
destination: { type: "string", description: "Hedef klasör (opsiyonel)" }
|
|
72
|
+
},
|
|
73
|
+
required: ["path"]
|
|
74
|
+
},
|
|
75
|
+
handler: async (params: { path?: string; destination?: string }, ctx) => {
|
|
76
|
+
const p = params.path?.trim();
|
|
77
|
+
if (!p) return { ok: false, output: "path zorunludur." };
|
|
78
|
+
const zipPath = resolveUserPath(ctx.cwd, p);
|
|
79
|
+
if (!existsSync(zipPath)) return { ok: false, output: `Dosya bulunamadı: ${p}` };
|
|
80
|
+
let destDir: string;
|
|
81
|
+
if (params.destination?.trim()) {
|
|
82
|
+
destDir = resolveUserPath(ctx.cwd, params.destination.trim());
|
|
83
|
+
if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
|
|
84
|
+
} else {
|
|
85
|
+
destDir = resolve(zipPath, "..");
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const zip = new AdmZip(zipPath);
|
|
89
|
+
const entries = zip.getEntries();
|
|
90
|
+
zip.extractAllTo(destDir, true);
|
|
91
|
+
const names = entries
|
|
92
|
+
.filter((e) => !e.isDirectory)
|
|
93
|
+
.slice(0, 30)
|
|
94
|
+
.map((e) => e.entryName);
|
|
95
|
+
const more = entries.length > 30 ? `\n… ve ${entries.length - 30} dosya daha` : "";
|
|
96
|
+
return { ok: true, output: `${entries.length} girdi açıldı → ${destDir}\n${names.join("\n")}${more}` };
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return { ok: false, output: `Açma başarısız: ${(err as Error).message}` };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|