@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,139 @@
|
|
|
1
|
+
export type ChatWrapperName =
|
|
2
|
+
| "auto"
|
|
3
|
+
| "gemma4"
|
|
4
|
+
| "gemma"
|
|
5
|
+
| "qwen3"
|
|
6
|
+
| "llama3"
|
|
7
|
+
| "mistral"
|
|
8
|
+
| "functionary"
|
|
9
|
+
| "chatml";
|
|
10
|
+
|
|
11
|
+
export type GpuBackend =
|
|
12
|
+
| "auto"
|
|
13
|
+
| "metal"
|
|
14
|
+
| "cuda"
|
|
15
|
+
| "vulkan"
|
|
16
|
+
| "webgpu"
|
|
17
|
+
| "cuda-llama"
|
|
18
|
+
| "vulkan-llama";
|
|
19
|
+
|
|
20
|
+
/** Düşünme (reasoning/thought) modu: auto = katalog değerini kullan */
|
|
21
|
+
export type ReasoningMode = "auto" | "on" | "off";
|
|
22
|
+
|
|
23
|
+
/** Model başına ayarlar (istemciden, katalogu değiştirmeden) */
|
|
24
|
+
export type ModelOverride = {
|
|
25
|
+
reasoning?: ReasoningMode;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ModelDefinition = {
|
|
29
|
+
/** Benzersiz model kimliği */
|
|
30
|
+
id: string;
|
|
31
|
+
/** Görünen ad */
|
|
32
|
+
name: string;
|
|
33
|
+
/** Açıklama */
|
|
34
|
+
description?: string;
|
|
35
|
+
/** İndirme URI'si (hf:..., https://...) */
|
|
36
|
+
uri: string;
|
|
37
|
+
/** Yerel dosya adı (yoksa uri'den türetilir) */
|
|
38
|
+
fileName?: string;
|
|
39
|
+
/** Bağlam penceresi boyutu */
|
|
40
|
+
contextSize?: number | "auto";
|
|
41
|
+
/** GPU katman sayısı */
|
|
42
|
+
gpuLayers?: number | "auto" | "max";
|
|
43
|
+
/** Chat sarmalayıcı adı */
|
|
44
|
+
chatWrapper?: ChatWrapperName;
|
|
45
|
+
/** Muhakeme (reasoning) modu — model başına geçersiz kılınabilir */
|
|
46
|
+
reasoning?: boolean;
|
|
47
|
+
/** Multimodal (görüntü) desteği */
|
|
48
|
+
multimodal?: boolean;
|
|
49
|
+
/** Dosya boyutu (bayt) — indirme kontrolü için */
|
|
50
|
+
size?: number;
|
|
51
|
+
/** Dosya sha256 sağlaması */
|
|
52
|
+
sha256?: string;
|
|
53
|
+
/** Modelin özel ana system promptu (varsa varsayılan prompt yerine kullanılır) */
|
|
54
|
+
systemPrompt?: string;
|
|
55
|
+
/** Modelin ikincil system promptu (ana prompttan sonra eklenir) */
|
|
56
|
+
secondarySystemPrompt?: string;
|
|
57
|
+
/** Özel yapılandırma (modele özgü parametreler) */
|
|
58
|
+
options?: Record<string, unknown>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type RemoteModelsRegistry = {
|
|
62
|
+
version: number;
|
|
63
|
+
source?: string;
|
|
64
|
+
updatedAt?: string;
|
|
65
|
+
models: ModelDefinition[];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type SessionConfig = {
|
|
69
|
+
temperature?: number;
|
|
70
|
+
topP?: number;
|
|
71
|
+
topK?: number;
|
|
72
|
+
maxTokens?: number;
|
|
73
|
+
seed?: number;
|
|
74
|
+
repeatPenalty?: number;
|
|
75
|
+
dryRepeatPenalty?: boolean;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type AjanConfig = {
|
|
79
|
+
/** Seçili model kimliği */
|
|
80
|
+
modelId: string;
|
|
81
|
+
/** Sistem promptu */
|
|
82
|
+
systemPrompt?: string;
|
|
83
|
+
/** Bağlam boyutu */
|
|
84
|
+
contextSize?: number | "auto";
|
|
85
|
+
/** GPU katman sayısı */
|
|
86
|
+
gpuLayers?: number | "auto" | "max";
|
|
87
|
+
/** GPU arka ucu */
|
|
88
|
+
gpu?: GpuBackend;
|
|
89
|
+
/** Model başına geçersiz kılmalar (örn. { "model-id": { reasoning: "off" } }) */
|
|
90
|
+
modelOverrides?: Record<string, ModelOverride>;
|
|
91
|
+
/** Oturum parametreleri */
|
|
92
|
+
session?: SessionConfig;
|
|
93
|
+
/** Araç çalıştırma zaman aşımı (ms) */
|
|
94
|
+
toolTimeout?: number;
|
|
95
|
+
/** Araçlarda döndürülen maks. çıktı uzunluğu (karakter) */
|
|
96
|
+
maxToolOutput?: number;
|
|
97
|
+
/** Context dolunca eski turların kırpılacağı eşik (0-1, contextSize oranı) */
|
|
98
|
+
contextTrimThreshold?: number;
|
|
99
|
+
/** Otonom görev başına maksimum araç-adımı (varsayılan 25) */
|
|
100
|
+
maxSteps?: number;
|
|
101
|
+
/** Çalıştırılması engellenecek komut desenleri (regex) */
|
|
102
|
+
blockedCommands?: string[];
|
|
103
|
+
/** Görev tamamlanınca bip çal */
|
|
104
|
+
beepOnComplete?: boolean;
|
|
105
|
+
/** Remote models.json kaynağı (GitHub vb.) */
|
|
106
|
+
remoteModelsUrl?: string;
|
|
107
|
+
/** Kayıtlı projeler (klasör yolları) */
|
|
108
|
+
projects?: string[];
|
|
109
|
+
/** Aktif proje yolu */
|
|
110
|
+
activeProject?: string;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export type ToolHandler<Params = any> = (params: Params, ctx: ToolExecutionContext) => Promise<ToolExecutionResult> | ToolExecutionResult;
|
|
114
|
+
|
|
115
|
+
export type ToolExecutionSignal = {
|
|
116
|
+
/** task_complete aracı çağrıldığında true olur */
|
|
117
|
+
taskCompleted?: boolean;
|
|
118
|
+
/** task_complete(continue:true) çağrıldığında model yeni bir tur istemiştir */
|
|
119
|
+
continueRequested?: boolean;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export type ToolExecutionContext = {
|
|
123
|
+
cwd: string;
|
|
124
|
+
config: AjanConfig;
|
|
125
|
+
signals?: ToolExecutionSignal;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export type ToolExecutionResult = {
|
|
129
|
+
ok: boolean;
|
|
130
|
+
output: string;
|
|
131
|
+
error?: string;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export type AgentTool = {
|
|
135
|
+
name: string;
|
|
136
|
+
description: string;
|
|
137
|
+
parameters: Record<string, unknown>;
|
|
138
|
+
handler: ToolHandler;
|
|
139
|
+
};
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import type { LlamaChatSession, ChatHistoryItem } from "node-llama-cpp";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { LlamaEngine, type ActiveGpu } from "../engine/llama.js";
|
|
4
|
+
import { buildSessionFunctions, createCoreTools, type ToolEvents } from "../tools/registry.js";
|
|
5
|
+
import type {
|
|
6
|
+
AgentTool,
|
|
7
|
+
AjanConfig,
|
|
8
|
+
ModelDefinition,
|
|
9
|
+
ReasoningMode,
|
|
10
|
+
ToolExecutionContext,
|
|
11
|
+
ToolExecutionSignal
|
|
12
|
+
} from "../config/types.js";
|
|
13
|
+
import { getModelById, loadConfig, resolveModelFilePath, buildDefaultSystemPrompt } from "../config/configManager.js";
|
|
14
|
+
import { loadProjectContext } from "../utils/projectContext.js";
|
|
15
|
+
import { logger } from "../utils/paths.js";
|
|
16
|
+
|
|
17
|
+
function historyItemText(item: ChatHistoryItem): string {
|
|
18
|
+
if (item.type === "system" || item.type === "user") {
|
|
19
|
+
return typeof item.text === "string" ? item.text : JSON.stringify(item.text);
|
|
20
|
+
}
|
|
21
|
+
return item.response.filter((p): p is string => typeof p === "string").join("\n");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type AgentEvents = {
|
|
25
|
+
/** Model token token üretirken (akış) */
|
|
26
|
+
onTextChunk?: (text: string) => void;
|
|
27
|
+
/** Model düşünce/akıl yürütme segmentleri üretirken (akış) */
|
|
28
|
+
onThinkingChunk?: (text: string) => void;
|
|
29
|
+
/** Bir araç çağrılmadan önce */
|
|
30
|
+
onToolCall?: (name: string, params: unknown) => void;
|
|
31
|
+
/** Bir araç sonucu döndüğünde */
|
|
32
|
+
onToolResult?: (name: string, ok: boolean, output: string) => void;
|
|
33
|
+
/** Nihai yanıt tamamlandığında */
|
|
34
|
+
onComplete?: (response: string) => void;
|
|
35
|
+
/** Otonom bir adım başladığında */
|
|
36
|
+
onStepStart?: (step: number, maxSteps: number) => void;
|
|
37
|
+
/** Otonom adım bittiğinde (o turdaki araç çağrısı sayısıyla) */
|
|
38
|
+
onStepEnd?: (step: number, maxSteps: number, toolCalls: number) => void;
|
|
39
|
+
/** Context sıkıştırıldığında (eski turlar kırpıldı) */
|
|
40
|
+
onContextTrimmed?: (removedItems: number) => void;
|
|
41
|
+
/** Hata oluştuğunda */
|
|
42
|
+
onError?: (error: Error) => void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type AgentOptions = {
|
|
46
|
+
model?: ModelDefinition;
|
|
47
|
+
config?: AjanConfig;
|
|
48
|
+
modelPath?: string;
|
|
49
|
+
cwd?: string;
|
|
50
|
+
tools?: AgentTool[];
|
|
51
|
+
events?: AgentEvents;
|
|
52
|
+
onLoadProgress?: (pct: number) => void;
|
|
53
|
+
onLoadComplete?: () => void;
|
|
54
|
+
signal?: AbortSignal;
|
|
55
|
+
temperature?: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type AjanTaskResult = {
|
|
59
|
+
response: string;
|
|
60
|
+
steps: number;
|
|
61
|
+
maxSteps: number;
|
|
62
|
+
completed: boolean;
|
|
63
|
+
budgetExhausted: boolean;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export class AjanAgent {
|
|
67
|
+
private engine: LlamaEngine | undefined;
|
|
68
|
+
private session: LlamaChatSession | undefined;
|
|
69
|
+
private readonly model: ModelDefinition;
|
|
70
|
+
private readonly config: AjanConfig;
|
|
71
|
+
private readonly modelPath: string;
|
|
72
|
+
private readonly cwd: string;
|
|
73
|
+
private systemPrompt: string;
|
|
74
|
+
private readonly tools: AgentTool[];
|
|
75
|
+
private events: AgentEvents;
|
|
76
|
+
private readonly onLoadProgress?: (pct: number) => void;
|
|
77
|
+
private readonly onLoadComplete?: () => void;
|
|
78
|
+
private readonly signal?: AbortSignal;
|
|
79
|
+
private readonly temperature?: number;
|
|
80
|
+
private pendingHistory: ChatHistoryItem[] | undefined;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Yayın hedefini değiştirir.
|
|
84
|
+
*
|
|
85
|
+
* Daemon tek modeli paylaştığı için her sohbet isteğinin çıktısı yalnızca
|
|
86
|
+
* soran istemciye gitmelidir. Ajanı yeniden kurmadan yönlendirme değiştirmek,
|
|
87
|
+
* yüklü modeli bozmadan yayını daraltmanın tek yoludur.
|
|
88
|
+
*/
|
|
89
|
+
setEvents(events: AgentEvents): void {
|
|
90
|
+
this.events = events;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
constructor(options: AgentOptions) {
|
|
94
|
+
const config = options.config ?? loadConfig();
|
|
95
|
+
this.config = config;
|
|
96
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
97
|
+
this.events = options.events ?? {};
|
|
98
|
+
this.onLoadProgress = options.onLoadProgress;
|
|
99
|
+
this.onLoadComplete = options.onLoadComplete;
|
|
100
|
+
this.signal = options.signal;
|
|
101
|
+
this.temperature = options.temperature;
|
|
102
|
+
|
|
103
|
+
if (options.model) {
|
|
104
|
+
this.model = options.model;
|
|
105
|
+
} else {
|
|
106
|
+
const id = config.modelId;
|
|
107
|
+
const found = getModelById(id);
|
|
108
|
+
if (!found) {
|
|
109
|
+
throw new Error(`Model bulunamadı: ${id}. 'ajan model list' ile uygun modelleri görün.`);
|
|
110
|
+
}
|
|
111
|
+
this.model = found;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this.modelPath = options.modelPath ?? resolveModelFilePath(this.model);
|
|
115
|
+
// Dosya yoksa node-llama-cpp'in ENOENT'sini yüzeye çıkarmak yerine
|
|
116
|
+
// ne yapılması gerektiğini söyleyen bir hata ver.
|
|
117
|
+
if (!existsSync(this.modelPath)) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Model dosyası bulunamadı: ${this.model.name} (${this.model.id}).\n` +
|
|
120
|
+
`Beklenen: ${this.modelPath}\n` +
|
|
121
|
+
`AJAN ile bu modeli indir: ajan models install ${this.model.id}`
|
|
122
|
+
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
// Sistem promptu her açılışta güncel çalışma diziniyle taze üretilir;
|
|
126
|
+
// config.systemPrompt yok sayılır (bayat cwd içermesin diye).
|
|
127
|
+
// Modelin özel system promptu varsa onu kullan, yoksa varsayılanı üret.
|
|
128
|
+
const modelSystemPrompt = this.model.systemPrompt;
|
|
129
|
+
const modelSecondaryPrompt = this.model.secondarySystemPrompt;
|
|
130
|
+
let basePrompt: string;
|
|
131
|
+
if (modelSystemPrompt) {
|
|
132
|
+
basePrompt =
|
|
133
|
+
modelSystemPrompt +
|
|
134
|
+
`\n- Çalışma dizini: ${this.cwd}. Dosya yollarında göreli yol kullan (örn. src/index.ts).\n` +
|
|
135
|
+
`- Deneme/test alanın: ${this.cwd}/.ajan. Deneme, prototip ve riskli testleri burada yap (open_playground); asıl projeye ancak doğruladıktan sonra uygula.`;
|
|
136
|
+
} else {
|
|
137
|
+
basePrompt = buildDefaultSystemPrompt(this.cwd);
|
|
138
|
+
}
|
|
139
|
+
this.systemPrompt =
|
|
140
|
+
basePrompt +
|
|
141
|
+
(modelSecondaryPrompt ? "\n\n" + modelSecondaryPrompt : "") +
|
|
142
|
+
loadProjectContext(this.cwd);
|
|
143
|
+
this.tools = options.tools ?? createCoreTools({ cwd: this.cwd, config });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Düşünme modunu çözer: kullanıcının model başına geçersiz kılması > katalog.
|
|
148
|
+
* "auto" katalogda tanımlı değilse undefined döner, sarmalayıcının varsayılanı kalır.
|
|
149
|
+
*/
|
|
150
|
+
private get reasoningEnabled(): boolean | undefined {
|
|
151
|
+
const override = this.config.modelOverrides?.[this.model.id]?.reasoning;
|
|
152
|
+
if (override === "on") return true;
|
|
153
|
+
if (override === "off") return false;
|
|
154
|
+
return this.model.reasoning;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** İstemciye raporlanacak düşünme durumu (auto değeri de çözülmüş haliyle) */
|
|
158
|
+
get reasoningInfo(): { mode: ReasoningMode; enabled: boolean } {
|
|
159
|
+
const mode = this.config.modelOverrides?.[this.model.id]?.reasoning ?? "auto";
|
|
160
|
+
return { mode, enabled: this.reasoningEnabled ?? false };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Motorun gerçekten kullandığı arka uç (null = henüz yüklenmedi) */
|
|
164
|
+
get activeGpu(): ActiveGpu | null {
|
|
165
|
+
return this.engine?.activeGpu ?? null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async gpuVramMb(): Promise<number> {
|
|
169
|
+
return (await this.engine?.gpuVramMb()) ?? 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Modelin kaç katmanı GPU'ya yüklendi (0 = henüz yüklenmedi) */
|
|
173
|
+
get gpuLayersUsed(): number {
|
|
174
|
+
return this.engine?.gpuLayersUsed ?? 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
get engineInfo(): { modelPath: string; modelId: string } {
|
|
178
|
+
return { modelPath: this.modelPath, modelId: this.model.id };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** İstemciye raporlanacak bağlam doluluk bilgisi */
|
|
182
|
+
getContextStats(): { usedTokens: number; contextSize: number } | null {
|
|
183
|
+
if (!this.engine) return null;
|
|
184
|
+
return {
|
|
185
|
+
usedTokens: this.engine.getUsedContextTokens(),
|
|
186
|
+
contextSize: this.engine.contextSize
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private async ensureSession(signal?: AbortSignal): Promise<LlamaChatSession> {
|
|
191
|
+
if (this.session) return this.session;
|
|
192
|
+
this.engine = new LlamaEngine({
|
|
193
|
+
modelPath: this.modelPath,
|
|
194
|
+
config: this.config,
|
|
195
|
+
chatWrapperName: this.model.chatWrapper,
|
|
196
|
+
reasoning: this.reasoningEnabled,
|
|
197
|
+
contextSize: this.model.contextSize,
|
|
198
|
+
gpuLayers: this.model.gpuLayers,
|
|
199
|
+
onProgress: (pct) => this.onLoadProgress?.(pct),
|
|
200
|
+
signal: signal ?? this.signal
|
|
201
|
+
});
|
|
202
|
+
await this.engine.createContext();
|
|
203
|
+
this.onLoadComplete?.();
|
|
204
|
+
this.session = this.engine.createSession(this.systemPrompt);
|
|
205
|
+
if (this.pendingHistory) {
|
|
206
|
+
this.session.setChatHistory(this.pendingHistory);
|
|
207
|
+
this.pendingHistory = undefined;
|
|
208
|
+
}
|
|
209
|
+
return this.session;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async prompt(userPrompt: string, options?: { signal?: AbortSignal }): Promise<string> {
|
|
213
|
+
const result = await this.promptTask(userPrompt, options);
|
|
214
|
+
return result.response;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Otonom görev yürütme: model araç çağrılarıyla görevi adım adım tamamlar.
|
|
219
|
+
* Döngü şuralarda biter: task_complete aracı, araçsız bir tur (nihai özet),
|
|
220
|
+
* adım bütçesi dolması veya iptal sinyali.
|
|
221
|
+
*/
|
|
222
|
+
async promptTask(userPrompt: string, options?: { signal?: AbortSignal }): Promise<AjanTaskResult> {
|
|
223
|
+
try {
|
|
224
|
+
return await this.runTask(userPrompt, options);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
// İptal (abort) bir hata değildir; yalnızca gerçek hataları bildir.
|
|
227
|
+
const aborted = err instanceof Error && (err.name === "AbortError" || /abort/i.test(err.message));
|
|
228
|
+
if (!aborted) {
|
|
229
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
230
|
+
logger.error(`Gorev hatasi: ${error.message}`);
|
|
231
|
+
this.events.onError?.(error);
|
|
232
|
+
}
|
|
233
|
+
throw err;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private async runTask(userPrompt: string, options?: { signal?: AbortSignal } | undefined): Promise<AjanTaskResult> {
|
|
238
|
+
const session = await this.ensureSession(options?.signal ?? this.signal);
|
|
239
|
+
const signals: ToolExecutionSignal = {};
|
|
240
|
+
const ctx: ToolExecutionContext = {
|
|
241
|
+
cwd: this.cwd,
|
|
242
|
+
config: this.config,
|
|
243
|
+
signals
|
|
244
|
+
};
|
|
245
|
+
let toolCallsThisRound = 0;
|
|
246
|
+
const toolEvents: ToolEvents = {
|
|
247
|
+
onToolCall: (name, params) => {
|
|
248
|
+
toolCallsThisRound++;
|
|
249
|
+
this.events.onToolCall?.(name, params);
|
|
250
|
+
},
|
|
251
|
+
onToolResult: (name, ok, output) => this.events.onToolResult?.(name, ok, output)
|
|
252
|
+
};
|
|
253
|
+
const functions = buildSessionFunctions(this.tools, ctx, toolEvents);
|
|
254
|
+
const sessionOpts = this.config.session ?? {};
|
|
255
|
+
const maxSteps = Math.max(1, this.config.maxSteps ?? 25);
|
|
256
|
+
const activeSignal = options?.signal ?? this.signal;
|
|
257
|
+
|
|
258
|
+
const runRound = (promptText: string): Promise<string> => {
|
|
259
|
+
toolCallsThisRound = 0;
|
|
260
|
+
this.trimContextIfNeeded();
|
|
261
|
+
logger.debug(`Prompt gonderiliyor (${promptText.length} karakter)`);
|
|
262
|
+
return session.prompt(promptText, {
|
|
263
|
+
functions,
|
|
264
|
+
documentFunctionParams: true,
|
|
265
|
+
temperature: this.temperature ?? sessionOpts.temperature,
|
|
266
|
+
topP: sessionOpts.topP,
|
|
267
|
+
topK: sessionOpts.topK,
|
|
268
|
+
maxTokens: sessionOpts.maxTokens,
|
|
269
|
+
seed: sessionOpts.seed,
|
|
270
|
+
onTextChunk: (text) => this.events.onTextChunk?.(text),
|
|
271
|
+
onResponseChunk: (chunk) => {
|
|
272
|
+
if (chunk.type === "segment" && chunk.segmentType === "thought") {
|
|
273
|
+
this.events.onThinkingChunk?.(chunk.text);
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
signal: activeSignal
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const continuePrompt =
|
|
281
|
+
"Devam et. (Bir önceki turda task_complete ile continue:true istedin veya işin yarıda kaldı.) " +
|
|
282
|
+
"Kalan adımları araçlarla tamamla. Görevi sürdürmek istersen yine task_complete(continue:true) çağır; " +
|
|
283
|
+
"her şey bitip doğrulandığında task_complete(continue:false) ile bitir.";
|
|
284
|
+
|
|
285
|
+
// Model kendi kararıyla sürdürür/durur: task_complete(continue:true) yeni tur açar,
|
|
286
|
+
// task_complete(continue:false) veya araçsız son yanıt akışı bitirir.
|
|
287
|
+
let step = 0;
|
|
288
|
+
let response = "";
|
|
289
|
+
while (step < maxSteps) {
|
|
290
|
+
if (activeSignal?.aborted) break;
|
|
291
|
+
step++;
|
|
292
|
+
this.events.onStepStart?.(step, maxSteps);
|
|
293
|
+
try {
|
|
294
|
+
response = await runRound(step === 1 ? userPrompt : continuePrompt);
|
|
295
|
+
} finally {
|
|
296
|
+
this.events.onStepEnd?.(step, maxSteps, toolCallsThisRound);
|
|
297
|
+
}
|
|
298
|
+
if (signals.taskCompleted) break;
|
|
299
|
+
const keepGoing = signals.continueRequested === true || toolCallsThisRound > 0;
|
|
300
|
+
signals.continueRequested = false;
|
|
301
|
+
if (!keepGoing) break;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const budgetExhausted = !signals.taskCompleted && step >= maxSteps;
|
|
305
|
+
this.events.onComplete?.(response);
|
|
306
|
+
return { response, steps: step, maxSteps, completed: signals.taskCompleted === true, budgetExhausted };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Context eşiğini aşınca eski konuşma turlarını baştan kırpar (system promptu korunur) */
|
|
310
|
+
private trimContextIfNeeded(): void {
|
|
311
|
+
if (!this.engine || !this.session) return;
|
|
312
|
+
const contextSize = this.engine.contextSize;
|
|
313
|
+
const used = this.engine.getUsedContextTokens();
|
|
314
|
+
if (contextSize <= 0 || used <= 0) return;
|
|
315
|
+
const threshold = this.config.contextTrimThreshold ?? 0.8;
|
|
316
|
+
if (used <= contextSize * threshold) return;
|
|
317
|
+
|
|
318
|
+
const history = this.session.getChatHistory();
|
|
319
|
+
if (history.length <= 1) return;
|
|
320
|
+
|
|
321
|
+
let systemCount = 0;
|
|
322
|
+
while (systemCount < history.length && history[systemCount]!.type === "system") systemCount++;
|
|
323
|
+
if (systemCount >= history.length) return;
|
|
324
|
+
|
|
325
|
+
const tail = history.slice(systemCount);
|
|
326
|
+
const budget = Math.floor(contextSize * threshold * 0.6);
|
|
327
|
+
const keptTail: ChatHistoryItem[] = [];
|
|
328
|
+
let total = 0;
|
|
329
|
+
for (let i = tail.length - 1; i >= 0; i--) {
|
|
330
|
+
const item = tail[i]!;
|
|
331
|
+
const tokens = Math.ceil(historyItemText(item).length / 3.2);
|
|
332
|
+
if (keptTail.length > 0 && total + tokens > budget) break;
|
|
333
|
+
keptTail.unshift(item);
|
|
334
|
+
total += tokens;
|
|
335
|
+
}
|
|
336
|
+
const kept = [...history.slice(0, systemCount), ...keptTail];
|
|
337
|
+
const removed = history.length - kept.length;
|
|
338
|
+
if (removed > 0) {
|
|
339
|
+
this.session.setChatHistory(kept);
|
|
340
|
+
logger.info(`Context sıkıştırıldı: ${removed} mesaj kırpıldı (${used} token)`);
|
|
341
|
+
this.events.onContextTrimmed?.(removed);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Oturumdaki geçmişi temizler (yeni konuşma) */
|
|
346
|
+
resetConversation(): void {
|
|
347
|
+
this.session?.resetChatHistory();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Aktif oturumun geçmişini döndürür (oturum yoksa bekleyen geçmişi) */
|
|
351
|
+
getChatHistory(): ChatHistoryItem[] {
|
|
352
|
+
if (this.session) return this.session.getChatHistory();
|
|
353
|
+
return this.pendingHistory ?? [];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Oturum geçmişini yükler; oturum henüz kurulmadıysa ilk kurulumda uygulanır */
|
|
357
|
+
setChatHistory(history: ChatHistoryItem[]): void {
|
|
358
|
+
if (this.session) {
|
|
359
|
+
this.session.setChatHistory(history);
|
|
360
|
+
this.pendingHistory = undefined;
|
|
361
|
+
} else {
|
|
362
|
+
this.pendingHistory = history;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Sistem promptunu değiştirir; aktif oturum varsa geçmiş temizlenip yeni sistem promptuyla yeniden kurulur */
|
|
367
|
+
async setSystemPrompt(prompt: string): Promise<void> {
|
|
368
|
+
this.systemPrompt = prompt;
|
|
369
|
+
if (this.engine && this.session) {
|
|
370
|
+
this.session = await this.engine.resetSession(prompt);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async dispose(): Promise<void> {
|
|
375
|
+
await this.engine?.dispose();
|
|
376
|
+
this.engine = undefined;
|
|
377
|
+
this.session = undefined;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GPU durum raporu.
|
|
3
|
+
*
|
|
4
|
+
* Önemli ayrım: "bu makine CUDA'yı destekliyor" ile "motor CUDA üzerinde
|
|
5
|
+
* çalışıyor" aynı şey değildir. Kurulu build'de CUDA ikilisi yoksa
|
|
6
|
+
* node-llama-cpp sessizce CPU'ya düşer. Bu yüzden `active` alanı yalnızca
|
|
7
|
+
* `Llama.gpu`'dan okunur, `supported` listesinden değil.
|
|
8
|
+
*/
|
|
9
|
+
import type { AjanService } from "../service.js";
|
|
10
|
+
import type { GpuReport } from "../protocol/messages.js";
|
|
11
|
+
|
|
12
|
+
export function backendLabel(id: string): string {
|
|
13
|
+
switch (id) {
|
|
14
|
+
case "cuda":
|
|
15
|
+
return "NVIDIA CUDA";
|
|
16
|
+
case "vulkan":
|
|
17
|
+
return "Vulkan";
|
|
18
|
+
case "metal":
|
|
19
|
+
return "Apple Metal";
|
|
20
|
+
case "cpu":
|
|
21
|
+
return "CPU";
|
|
22
|
+
default:
|
|
23
|
+
return id;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function buildGpuReport(service: AjanService): Promise<GpuReport> {
|
|
28
|
+
const status = await service.getGpuStatus();
|
|
29
|
+
return {
|
|
30
|
+
active: status.active?.backend ?? "yüklenmedi",
|
|
31
|
+
offload: status.active?.offload ?? false,
|
|
32
|
+
vramMb: status.vramMb,
|
|
33
|
+
layers: status.layers,
|
|
34
|
+
supported: status.supported,
|
|
35
|
+
preferred: status.preferred,
|
|
36
|
+
fallback: status.fallback
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Tek satırlık insan okunur özet (CLI `status` çıktısı için). */
|
|
41
|
+
export function formatGpu(gpu: GpuReport): string {
|
|
42
|
+
if (gpu.active === "yüklenmedi") {
|
|
43
|
+
const candidates = gpu.supported.length > 0 ? gpu.supported.join(", ") : "yok";
|
|
44
|
+
return `yüklenmedi (makinede: ${candidates})`;
|
|
45
|
+
}
|
|
46
|
+
const parts = [backendLabel(gpu.active)];
|
|
47
|
+
if (gpu.active !== "cpu") {
|
|
48
|
+
parts.push(gpu.offload ? "katman aktarımı açık" : "katman aktarımı yok");
|
|
49
|
+
if (gpu.layers > 0) parts.push(`${gpu.layers} katman`);
|
|
50
|
+
if (gpu.vramMb > 0) parts.push(`${gpu.vramMb} MB VRAM`);
|
|
51
|
+
}
|
|
52
|
+
if (gpu.fallback) parts.push(`istenen ${gpu.preferred} kullanılamadı`);
|
|
53
|
+
return parts.join(" · ");
|
|
54
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ajan-engine` giriş noktası — motor sürecinin kendisi.
|
|
3
|
+
*
|
|
4
|
+
* İstemciler buraya bağlanır; bu süreç kapandığında tüm oturumlar biter.
|
|
5
|
+
* Çalışma dizini ve socket yolu komut satırından verilebilir.
|
|
6
|
+
*/
|
|
7
|
+
import { Runner } from "./runner.js";
|
|
8
|
+
import { DaemonServer } from "./server.js";
|
|
9
|
+
import { AJAN_VERSION } from "../version.js";
|
|
10
|
+
import { defaultSocketPath } from "../protocol/channel.js";
|
|
11
|
+
import { logger, setLogLevel } from "../utils/paths.js";
|
|
12
|
+
|
|
13
|
+
type Options = { cwd: string; socket: string; logLevel?: "debug" | "info" | "warn" | "error" };
|
|
14
|
+
|
|
15
|
+
function parseArgs(argv: string[]): Options {
|
|
16
|
+
const opts: Options = { cwd: process.cwd(), socket: defaultSocketPath() };
|
|
17
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
|
+
const a = argv[i];
|
|
19
|
+
if (a === "--cwd" || a === "-C") opts.cwd = argv[++i] ?? opts.cwd;
|
|
20
|
+
else if (a === "--socket") opts.socket = argv[++i] ?? opts.socket;
|
|
21
|
+
else if (a === "--log-level") {
|
|
22
|
+
const v = argv[++i];
|
|
23
|
+
if (v === "debug" || v === "info" || v === "warn" || v === "error") opts.logLevel = v;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return opts;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function usage(): void {
|
|
30
|
+
process.stdout.write(
|
|
31
|
+
`ajan-engine ${AJAN_VERSION} — AJAN motoru (IPC)\n\n` +
|
|
32
|
+
`Kullanım: ajan-engine [seçenekler]\n\n` +
|
|
33
|
+
` -C, --cwd <yol> ajanın çalışma dizini (varsayılan: ${process.cwd()})\n` +
|
|
34
|
+
` --socket <yol> dinlenecek socket (varsayılan: ${defaultSocketPath()})\n` +
|
|
35
|
+
` --log-level <seviye> debug | info | warn | error\n` +
|
|
36
|
+
` -h, --help bu yardım\n`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function main(): Promise<void> {
|
|
41
|
+
const argv = process.argv.slice(2);
|
|
42
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
43
|
+
usage();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const opts = parseArgs(argv);
|
|
48
|
+
if (opts.logLevel) setLogLevel(opts.logLevel);
|
|
49
|
+
|
|
50
|
+
const runner = new Runner(opts.cwd);
|
|
51
|
+
const server = new DaemonServer(runner, opts.socket);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await server.listen();
|
|
55
|
+
} catch (err) {
|
|
56
|
+
const e = err as NodeJS.ErrnoException;
|
|
57
|
+
if (e.code === "EADDRINUSE") {
|
|
58
|
+
process.stderr.write(`AJAN motoru zaten çalışıyor: ${opts.socket}\n`);
|
|
59
|
+
process.exit(3);
|
|
60
|
+
}
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
process.stdout.write(`AJAN motoru hazır ajan=${AJAN_VERSION} socket=${opts.socket} cwd=${opts.cwd}\n`);
|
|
65
|
+
|
|
66
|
+
let closing = false;
|
|
67
|
+
const shutdown = async (signal: string) => {
|
|
68
|
+
if (closing) return;
|
|
69
|
+
closing = true;
|
|
70
|
+
logger.info(`kapanıyor (${signal})`);
|
|
71
|
+
await server.close();
|
|
72
|
+
process.exit(0);
|
|
73
|
+
};
|
|
74
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
75
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
76
|
+
process.on("uncaughtException", (err) => {
|
|
77
|
+
logger.error(`yakalanmamış hata: ${err.message}`);
|
|
78
|
+
});
|
|
79
|
+
process.on("unhandledRejection", (err) => {
|
|
80
|
+
logger.error(`işlenmemiş reddetme: ${String(err)}`);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
main().catch((err) => {
|
|
85
|
+
process.stderr.write(`motor başlatılamadı: ${(err as Error).message}\n`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
});
|