titan-agent 2026.4.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/.env.example +21 -0
- package/LICENSE +29 -0
- package/README.md +305 -0
- package/assets/titan-logo.png +0 -0
- package/dist/agent/agent.js +1458 -0
- package/dist/agent/agent.js.map +1 -0
- package/dist/agent/generator.js +1078 -0
- package/dist/agent/generator.js.map +1 -0
- package/dist/cli/index.js +5064 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/gateway/server.js +4462 -0
- package/dist/gateway/server.js.map +1 -0
- package/dist/skills/builtin/apply_patch.js +368 -0
- package/dist/skills/builtin/apply_patch.js.map +1 -0
- package/dist/skills/builtin/auto_generate.js +1129 -0
- package/dist/skills/builtin/auto_generate.js.map +1 -0
- package/dist/skills/builtin/browser.js +357 -0
- package/dist/skills/builtin/browser.js.map +1 -0
- package/dist/skills/builtin/cron.js +410 -0
- package/dist/skills/builtin/cron.js.map +1 -0
- package/dist/skills/builtin/filesystem.js +386 -0
- package/dist/skills/builtin/filesystem.js.map +1 -0
- package/dist/skills/builtin/memory_skill.js +430 -0
- package/dist/skills/builtin/memory_skill.js.map +1 -0
- package/dist/skills/builtin/process.js +421 -0
- package/dist/skills/builtin/process.js.map +1 -0
- package/dist/skills/builtin/sessions.js +1729 -0
- package/dist/skills/builtin/sessions.js.map +1 -0
- package/dist/skills/builtin/shell.js +327 -0
- package/dist/skills/builtin/shell.js.map +1 -0
- package/dist/skills/builtin/vision.js +491 -0
- package/dist/skills/builtin/vision.js.map +1 -0
- package/dist/skills/builtin/voice.js +468 -0
- package/dist/skills/builtin/voice.js.map +1 -0
- package/dist/skills/builtin/web_fetch.js +331 -0
- package/dist/skills/builtin/web_fetch.js.map +1 -0
- package/dist/skills/builtin/web_search.js +317 -0
- package/dist/skills/builtin/web_search.js.map +1 -0
- package/dist/skills/builtin/webhook.js +323 -0
- package/dist/skills/builtin/webhook.js.map +1 -0
- package/dist/skills/registry.js +3369 -0
- package/dist/skills/registry.js.map +1 -0
- package/package.json +118 -0
|
@@ -0,0 +1,1458 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/utils/logger.ts
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
function formatTimestamp() {
|
|
15
|
+
return chalk.gray((/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19));
|
|
16
|
+
}
|
|
17
|
+
function log(level, component, message, ...args) {
|
|
18
|
+
if (level < currentLevel) return;
|
|
19
|
+
const prefix = `${formatTimestamp()} ${LEVEL_LABELS[level]} ${chalk.blue(`[${component}]`)}`;
|
|
20
|
+
console.log(`${prefix} ${message}`, ...args);
|
|
21
|
+
}
|
|
22
|
+
var LEVEL_LABELS, currentLevel, logger, logger_default;
|
|
23
|
+
var init_logger = __esm({
|
|
24
|
+
"src/utils/logger.ts"() {
|
|
25
|
+
"use strict";
|
|
26
|
+
LEVEL_LABELS = {
|
|
27
|
+
[0 /* DEBUG */]: chalk.gray("DEBUG"),
|
|
28
|
+
[1 /* INFO */]: chalk.cyan("INFO "),
|
|
29
|
+
[2 /* WARN */]: chalk.yellow("WARN "),
|
|
30
|
+
[3 /* ERROR */]: chalk.red("ERROR"),
|
|
31
|
+
[4 /* SILENT */]: ""
|
|
32
|
+
};
|
|
33
|
+
currentLevel = 1 /* INFO */;
|
|
34
|
+
logger = {
|
|
35
|
+
debug: (component, msg, ...args) => log(0 /* DEBUG */, component, msg, ...args),
|
|
36
|
+
info: (component, msg, ...args) => log(1 /* INFO */, component, msg, ...args),
|
|
37
|
+
warn: (component, msg, ...args) => log(2 /* WARN */, component, msg, ...args),
|
|
38
|
+
error: (component, msg, ...args) => log(3 /* ERROR */, component, msg, ...args)
|
|
39
|
+
};
|
|
40
|
+
logger_default = logger;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// src/security/encryption.ts
|
|
45
|
+
var encryption_exports = {};
|
|
46
|
+
__export(encryption_exports, {
|
|
47
|
+
decrypt: () => decrypt,
|
|
48
|
+
encrypt: () => encrypt,
|
|
49
|
+
generateKey: () => generateKey
|
|
50
|
+
});
|
|
51
|
+
import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
|
|
52
|
+
function generateKey() {
|
|
53
|
+
return randomBytes(32);
|
|
54
|
+
}
|
|
55
|
+
function encrypt(text, key) {
|
|
56
|
+
try {
|
|
57
|
+
const keyBuffer = typeof key === "string" ? Buffer.from(key, "hex") : key;
|
|
58
|
+
if (keyBuffer.length !== 32) {
|
|
59
|
+
throw new Error("Encryption key must be exactly 32 bytes (256 bits).");
|
|
60
|
+
}
|
|
61
|
+
const iv = randomBytes(16);
|
|
62
|
+
const cipher = createCipheriv(ALGORITHM, keyBuffer, iv);
|
|
63
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
64
|
+
encrypted += cipher.final("hex");
|
|
65
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
66
|
+
return {
|
|
67
|
+
iv: iv.toString("hex"),
|
|
68
|
+
authTag,
|
|
69
|
+
data: encrypted
|
|
70
|
+
};
|
|
71
|
+
} catch (e) {
|
|
72
|
+
logger_default.error(COMPONENT7, `Encryption failed: ${e.message}`);
|
|
73
|
+
throw new Error("Failed to encrypt session data.");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function decrypt(payload, key) {
|
|
77
|
+
try {
|
|
78
|
+
const keyBuffer = typeof key === "string" ? Buffer.from(key, "hex") : key;
|
|
79
|
+
if (keyBuffer.length !== 32) {
|
|
80
|
+
throw new Error("Decryption key must be exactly 32 bytes (256 bits).");
|
|
81
|
+
}
|
|
82
|
+
const decipher = createDecipheriv(ALGORITHM, keyBuffer, Buffer.from(payload.iv, "hex"));
|
|
83
|
+
decipher.setAuthTag(Buffer.from(payload.authTag, "hex"));
|
|
84
|
+
let decrypted = decipher.update(payload.data, "hex", "utf8");
|
|
85
|
+
decrypted += decipher.final("utf8");
|
|
86
|
+
return decrypted;
|
|
87
|
+
} catch (e) {
|
|
88
|
+
logger_default.error(COMPONENT7, `Decryption failed: ${e.message}`);
|
|
89
|
+
throw new Error("Failed to decrypt session data. Invalid key or corrupted payload.");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
var COMPONENT7, ALGORITHM;
|
|
93
|
+
var init_encryption = __esm({
|
|
94
|
+
"src/security/encryption.ts"() {
|
|
95
|
+
"use strict";
|
|
96
|
+
init_logger();
|
|
97
|
+
COMPONENT7 = "Encryption";
|
|
98
|
+
ALGORITHM = "aes-256-gcm";
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// src/agent/agent.ts
|
|
103
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
104
|
+
|
|
105
|
+
// src/providers/base.ts
|
|
106
|
+
var LLMProvider = class {
|
|
107
|
+
/** Get the provider identifier from a model string like "anthropic/claude-3" */
|
|
108
|
+
static parseModelId(modelId) {
|
|
109
|
+
const parts = modelId.split("/");
|
|
110
|
+
if (parts.length >= 2) {
|
|
111
|
+
return { provider: parts[0], model: parts.slice(1).join("/") };
|
|
112
|
+
}
|
|
113
|
+
return { provider: "anthropic", model: modelId };
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// src/config/config.ts
|
|
118
|
+
import { existsSync as existsSync2 } from "fs";
|
|
119
|
+
|
|
120
|
+
// src/utils/constants.ts
|
|
121
|
+
import { homedir } from "os";
|
|
122
|
+
import { join } from "path";
|
|
123
|
+
var TITAN_VERSION = "2026.4.0";
|
|
124
|
+
var TITAN_NAME = "TITAN";
|
|
125
|
+
var TITAN_ASCII_LOGO = `
|
|
126
|
+
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
127
|
+
\u2551 \u2551
|
|
128
|
+
\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2551
|
|
129
|
+
\u2551 \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2551
|
|
130
|
+
\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2551
|
|
131
|
+
\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 \u2551
|
|
132
|
+
\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 \u2551
|
|
133
|
+
\u2551 \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u2551
|
|
134
|
+
\u2551 \u2551
|
|
135
|
+
\u2551 The Intelligent Task Automation Network \u2551
|
|
136
|
+
\u2551 v${TITAN_VERSION} \u2551
|
|
137
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D`;
|
|
138
|
+
var TITAN_HOME = join(homedir(), ".titan");
|
|
139
|
+
var TITAN_CONFIG_PATH = join(TITAN_HOME, "titan.json");
|
|
140
|
+
var TITAN_DB_PATH = join(TITAN_HOME, "titan.db");
|
|
141
|
+
var TITAN_WORKSPACE = join(TITAN_HOME, "workspace");
|
|
142
|
+
var TITAN_SKILLS_DIR = join(TITAN_WORKSPACE, "skills");
|
|
143
|
+
var TITAN_LOGS_DIR = join(TITAN_HOME, "logs");
|
|
144
|
+
var TITAN_MEMORY_DIR = join(TITAN_HOME, "memory");
|
|
145
|
+
var AGENTS_MD = join(TITAN_WORKSPACE, "AGENTS.md");
|
|
146
|
+
var SOUL_MD = join(TITAN_WORKSPACE, "SOUL.md");
|
|
147
|
+
var TOOLS_MD = join(TITAN_WORKSPACE, "TOOLS.md");
|
|
148
|
+
var DEFAULT_GATEWAY_HOST = "127.0.0.1";
|
|
149
|
+
var DEFAULT_GATEWAY_PORT = 18789;
|
|
150
|
+
var DEFAULT_WEB_PORT = 18790;
|
|
151
|
+
var DEFAULT_MODEL = "anthropic/claude-sonnet-4-20250514";
|
|
152
|
+
var DEFAULT_MAX_TOKENS = 8192;
|
|
153
|
+
var DEFAULT_TEMPERATURE = 0.7;
|
|
154
|
+
var MAX_CONTEXT_MESSAGES = 50;
|
|
155
|
+
var SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
156
|
+
var DEFAULT_SANDBOX_MODE = "host";
|
|
157
|
+
var ALLOWED_TOOLS_DEFAULT = [
|
|
158
|
+
"shell",
|
|
159
|
+
"read_file",
|
|
160
|
+
"write_file",
|
|
161
|
+
"edit_file",
|
|
162
|
+
"list_dir",
|
|
163
|
+
"web_search",
|
|
164
|
+
"browser",
|
|
165
|
+
"cron",
|
|
166
|
+
"webhook",
|
|
167
|
+
"email",
|
|
168
|
+
"memory"
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
// src/utils/helpers.ts
|
|
172
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
173
|
+
function ensureDir(dirPath) {
|
|
174
|
+
if (!existsSync(dirPath)) {
|
|
175
|
+
mkdirSync(dirPath, { recursive: true });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function readJsonFile(filePath) {
|
|
179
|
+
try {
|
|
180
|
+
if (!existsSync(filePath)) return null;
|
|
181
|
+
const content = readFileSync(filePath, "utf-8");
|
|
182
|
+
return JSON.parse(content);
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/config/schema.ts
|
|
189
|
+
import { z } from "zod";
|
|
190
|
+
var ProviderConfigSchema = z.object({
|
|
191
|
+
apiKey: z.string().optional(),
|
|
192
|
+
baseUrl: z.string().optional(),
|
|
193
|
+
model: z.string().optional(),
|
|
194
|
+
maxTokens: z.number().optional(),
|
|
195
|
+
temperature: z.number().min(0).max(2).optional()
|
|
196
|
+
});
|
|
197
|
+
var ChannelConfigSchema = z.object({
|
|
198
|
+
enabled: z.boolean().default(false),
|
|
199
|
+
token: z.string().optional(),
|
|
200
|
+
apiKey: z.string().optional(),
|
|
201
|
+
allowFrom: z.array(z.string()).default([]),
|
|
202
|
+
dmPolicy: z.enum(["pairing", "open", "closed"]).default("pairing")
|
|
203
|
+
});
|
|
204
|
+
var SecurityConfigSchema = z.object({
|
|
205
|
+
sandboxMode: z.enum(["host", "docker", "none"]).default(DEFAULT_SANDBOX_MODE),
|
|
206
|
+
allowedTools: z.array(z.string()).default(ALLOWED_TOOLS_DEFAULT),
|
|
207
|
+
deniedTools: z.array(z.string()).default([]),
|
|
208
|
+
maxConcurrentTasks: z.number().default(5),
|
|
209
|
+
commandTimeout: z.number().default(3e4),
|
|
210
|
+
fileSystemAllowlist: z.array(z.string()).default([]),
|
|
211
|
+
networkAllowlist: z.array(z.string()).default(["*"]),
|
|
212
|
+
shield: z.object({
|
|
213
|
+
enabled: z.boolean().default(true),
|
|
214
|
+
mode: z.enum(["standard", "strict"]).default("strict")
|
|
215
|
+
}).default({})
|
|
216
|
+
});
|
|
217
|
+
var GatewayConfigSchema = z.object({
|
|
218
|
+
host: z.string().default(DEFAULT_GATEWAY_HOST),
|
|
219
|
+
port: z.number().default(DEFAULT_GATEWAY_PORT),
|
|
220
|
+
webPort: z.number().default(DEFAULT_WEB_PORT),
|
|
221
|
+
auth: z.object({
|
|
222
|
+
mode: z.enum(["none", "token", "password"]).default("token"),
|
|
223
|
+
token: z.string().optional(),
|
|
224
|
+
password: z.string().optional()
|
|
225
|
+
}).default({})
|
|
226
|
+
});
|
|
227
|
+
var AgentConfigSchema = z.object({
|
|
228
|
+
model: z.string().default(DEFAULT_MODEL),
|
|
229
|
+
maxTokens: z.number().default(DEFAULT_MAX_TOKENS),
|
|
230
|
+
temperature: z.number().default(DEFAULT_TEMPERATURE),
|
|
231
|
+
systemPrompt: z.string().optional(),
|
|
232
|
+
workspace: z.string().optional(),
|
|
233
|
+
thinkingMode: z.enum(["off", "low", "medium", "high"]).default("medium")
|
|
234
|
+
});
|
|
235
|
+
var TitanConfigSchema = z.object({
|
|
236
|
+
agent: AgentConfigSchema.default({}),
|
|
237
|
+
providers: z.object({
|
|
238
|
+
anthropic: ProviderConfigSchema.default({}),
|
|
239
|
+
openai: ProviderConfigSchema.default({}),
|
|
240
|
+
google: ProviderConfigSchema.default({}),
|
|
241
|
+
ollama: ProviderConfigSchema.default({})
|
|
242
|
+
}).default({}),
|
|
243
|
+
channels: z.object({
|
|
244
|
+
discord: ChannelConfigSchema.default({}),
|
|
245
|
+
telegram: ChannelConfigSchema.default({}),
|
|
246
|
+
slack: ChannelConfigSchema.default({}),
|
|
247
|
+
whatsapp: ChannelConfigSchema.default({}),
|
|
248
|
+
webchat: ChannelConfigSchema.default({}),
|
|
249
|
+
googlechat: ChannelConfigSchema.default({}),
|
|
250
|
+
matrix: ChannelConfigSchema.default({}),
|
|
251
|
+
signal: ChannelConfigSchema.default({}),
|
|
252
|
+
msteams: ChannelConfigSchema.default({}),
|
|
253
|
+
bluebubbles: ChannelConfigSchema.default({})
|
|
254
|
+
}).default({}),
|
|
255
|
+
gateway: GatewayConfigSchema.default({}),
|
|
256
|
+
security: SecurityConfigSchema.default({}),
|
|
257
|
+
memory: z.object({
|
|
258
|
+
enabled: z.boolean().default(true),
|
|
259
|
+
maxHistoryMessages: z.number().default(50),
|
|
260
|
+
vectorSearchEnabled: z.boolean().default(false)
|
|
261
|
+
}).default({}),
|
|
262
|
+
skills: z.object({
|
|
263
|
+
enabled: z.boolean().default(true),
|
|
264
|
+
autoDiscover: z.boolean().default(true),
|
|
265
|
+
marketplace: z.boolean().default(false)
|
|
266
|
+
}).default({}),
|
|
267
|
+
logging: z.object({
|
|
268
|
+
level: z.enum(["debug", "info", "warn", "error", "silent"]).default("info"),
|
|
269
|
+
file: z.boolean().default(true)
|
|
270
|
+
}).default({}),
|
|
271
|
+
autonomy: z.object({
|
|
272
|
+
/** autonomous = full auto, supervised = asks for dangerous ops, locked = asks for everything */
|
|
273
|
+
mode: z.enum(["autonomous", "supervised", "locked"]).default("supervised"),
|
|
274
|
+
/** Auto-approve moderate-risk tools in main session (cli/webchat) */
|
|
275
|
+
autoApproveMainSession: z.boolean().default(true),
|
|
276
|
+
/** Timeout for HITL approval requests (ms). Auto-deny after timeout. */
|
|
277
|
+
approvalTimeoutMs: z.number().default(6e4),
|
|
278
|
+
/** Notify user of auto-approved actions */
|
|
279
|
+
notifyOnAutoApprove: z.boolean().default(true)
|
|
280
|
+
}).default({})
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// src/config/config.ts
|
|
284
|
+
init_logger();
|
|
285
|
+
var COMPONENT = "Config";
|
|
286
|
+
var cachedConfig = null;
|
|
287
|
+
function getDefaultConfig() {
|
|
288
|
+
return TitanConfigSchema.parse({});
|
|
289
|
+
}
|
|
290
|
+
function loadConfig() {
|
|
291
|
+
if (cachedConfig) return cachedConfig;
|
|
292
|
+
ensureDir(TITAN_HOME);
|
|
293
|
+
let rawConfig = {};
|
|
294
|
+
if (existsSync2(TITAN_CONFIG_PATH)) {
|
|
295
|
+
const loaded = readJsonFile(TITAN_CONFIG_PATH);
|
|
296
|
+
if (loaded) {
|
|
297
|
+
rawConfig = loaded;
|
|
298
|
+
logger_default.debug(COMPONENT, `Loaded config from ${TITAN_CONFIG_PATH}`);
|
|
299
|
+
} else {
|
|
300
|
+
logger_default.warn(COMPONENT, `Failed to parse config at ${TITAN_CONFIG_PATH}, using defaults`);
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
logger_default.info(COMPONENT, "No config file found, using defaults");
|
|
304
|
+
}
|
|
305
|
+
applyEnvOverrides(rawConfig);
|
|
306
|
+
const result = TitanConfigSchema.safeParse(rawConfig);
|
|
307
|
+
if (!result.success) {
|
|
308
|
+
logger_default.warn(COMPONENT, "Config validation issues, using defaults for invalid fields");
|
|
309
|
+
logger_default.debug(COMPONENT, result.error.message);
|
|
310
|
+
cachedConfig = getDefaultConfig();
|
|
311
|
+
} else {
|
|
312
|
+
cachedConfig = result.data;
|
|
313
|
+
}
|
|
314
|
+
return cachedConfig;
|
|
315
|
+
}
|
|
316
|
+
function applyEnvOverrides(config) {
|
|
317
|
+
const envMap = {
|
|
318
|
+
TITAN_MODEL: (val) => setNested(config, "agent.model", val),
|
|
319
|
+
TITAN_GATEWAY_PORT: (val) => setNested(config, "gateway.port", parseInt(val, 10)),
|
|
320
|
+
TITAN_GATEWAY_HOST: (val) => setNested(config, "gateway.host", val),
|
|
321
|
+
TITAN_LOG_LEVEL: (val) => setNested(config, "logging.level", val),
|
|
322
|
+
ANTHROPIC_API_KEY: (val) => setNested(config, "providers.anthropic.apiKey", val),
|
|
323
|
+
OPENAI_API_KEY: (val) => setNested(config, "providers.openai.apiKey", val),
|
|
324
|
+
GOOGLE_API_KEY: (val) => setNested(config, "providers.google.apiKey", val),
|
|
325
|
+
OLLAMA_BASE_URL: (val) => setNested(config, "providers.ollama.baseUrl", val),
|
|
326
|
+
DISCORD_TOKEN: (val) => setNested(config, "channels.discord.token", val),
|
|
327
|
+
TELEGRAM_TOKEN: (val) => setNested(config, "channels.telegram.token", val),
|
|
328
|
+
SLACK_TOKEN: (val) => setNested(config, "channels.slack.token", val)
|
|
329
|
+
};
|
|
330
|
+
for (const [envKey, setter] of Object.entries(envMap)) {
|
|
331
|
+
const val = process.env[envKey];
|
|
332
|
+
if (val) {
|
|
333
|
+
setter(val);
|
|
334
|
+
logger_default.debug(COMPONENT, `Applied env override: ${envKey}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function setNested(obj, path, value) {
|
|
339
|
+
const parts = path.split(".");
|
|
340
|
+
let current = obj;
|
|
341
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
342
|
+
if (!current[parts[i]] || typeof current[parts[i]] !== "object") {
|
|
343
|
+
current[parts[i]] = {};
|
|
344
|
+
}
|
|
345
|
+
current = current[parts[i]];
|
|
346
|
+
}
|
|
347
|
+
current[parts[parts.length - 1]] = value;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/providers/anthropic.ts
|
|
351
|
+
init_logger();
|
|
352
|
+
import { v4 as uuid } from "uuid";
|
|
353
|
+
var COMPONENT2 = "Anthropic";
|
|
354
|
+
var AnthropicProvider = class extends LLMProvider {
|
|
355
|
+
name = "anthropic";
|
|
356
|
+
displayName = "Anthropic (Claude)";
|
|
357
|
+
get apiKey() {
|
|
358
|
+
const config = loadConfig();
|
|
359
|
+
return config.providers.anthropic.apiKey || process.env.ANTHROPIC_API_KEY || "";
|
|
360
|
+
}
|
|
361
|
+
get baseUrl() {
|
|
362
|
+
const config = loadConfig();
|
|
363
|
+
return config.providers.anthropic.baseUrl || "https://api.anthropic.com";
|
|
364
|
+
}
|
|
365
|
+
async chat(options) {
|
|
366
|
+
const model = options.model || "claude-sonnet-4-20250514";
|
|
367
|
+
const apiKey = this.apiKey;
|
|
368
|
+
if (!apiKey) throw new Error("Anthropic API key not configured");
|
|
369
|
+
logger_default.debug(COMPONENT2, `Chat request: model=${model}, messages=${options.messages.length}`);
|
|
370
|
+
const systemMessage = options.messages.find((m) => m.role === "system");
|
|
371
|
+
const nonSystemMessages = options.messages.filter((m) => m.role !== "system");
|
|
372
|
+
const body = {
|
|
373
|
+
model: model.replace("anthropic/", ""),
|
|
374
|
+
max_tokens: options.maxTokens || 8192,
|
|
375
|
+
messages: nonSystemMessages.map((m) => ({
|
|
376
|
+
role: m.role === "tool" ? "user" : m.role,
|
|
377
|
+
content: m.role === "tool" ? [{ type: "tool_result", tool_use_id: m.toolCallId, content: m.content }] : m.content
|
|
378
|
+
}))
|
|
379
|
+
};
|
|
380
|
+
if (systemMessage) {
|
|
381
|
+
body.system = systemMessage.content;
|
|
382
|
+
}
|
|
383
|
+
if (options.tools && options.tools.length > 0) {
|
|
384
|
+
body.tools = options.tools.map((t) => ({
|
|
385
|
+
name: t.function.name,
|
|
386
|
+
description: t.function.description,
|
|
387
|
+
input_schema: t.function.parameters
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
if (options.temperature !== void 0) {
|
|
391
|
+
body.temperature = options.temperature;
|
|
392
|
+
}
|
|
393
|
+
const response = await fetch(`${this.baseUrl}/v1/messages`, {
|
|
394
|
+
method: "POST",
|
|
395
|
+
headers: {
|
|
396
|
+
"Content-Type": "application/json",
|
|
397
|
+
"x-api-key": apiKey,
|
|
398
|
+
"anthropic-version": "2023-06-01"
|
|
399
|
+
},
|
|
400
|
+
body: JSON.stringify(body)
|
|
401
|
+
});
|
|
402
|
+
if (!response.ok) {
|
|
403
|
+
const errorText = await response.text();
|
|
404
|
+
throw new Error(`Anthropic API error (${response.status}): ${errorText}`);
|
|
405
|
+
}
|
|
406
|
+
const data = await response.json();
|
|
407
|
+
const content = data.content;
|
|
408
|
+
let textContent = "";
|
|
409
|
+
const toolCalls = [];
|
|
410
|
+
for (const block of content) {
|
|
411
|
+
if (block.type === "text") {
|
|
412
|
+
textContent += block.text;
|
|
413
|
+
} else if (block.type === "tool_use") {
|
|
414
|
+
toolCalls.push({
|
|
415
|
+
id: block.id,
|
|
416
|
+
type: "function",
|
|
417
|
+
function: {
|
|
418
|
+
name: block.name,
|
|
419
|
+
arguments: JSON.stringify(block.input)
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const usage = data.usage;
|
|
425
|
+
return {
|
|
426
|
+
id: data.id || uuid(),
|
|
427
|
+
content: textContent,
|
|
428
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
429
|
+
usage: usage ? {
|
|
430
|
+
promptTokens: usage.input_tokens,
|
|
431
|
+
completionTokens: usage.output_tokens,
|
|
432
|
+
totalTokens: usage.input_tokens + usage.output_tokens
|
|
433
|
+
} : void 0,
|
|
434
|
+
finishReason: toolCalls.length > 0 ? "tool_calls" : "stop",
|
|
435
|
+
model
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
async *chatStream(options) {
|
|
439
|
+
try {
|
|
440
|
+
const response = await this.chat(options);
|
|
441
|
+
if (response.content) {
|
|
442
|
+
yield { type: "text", content: response.content };
|
|
443
|
+
}
|
|
444
|
+
if (response.toolCalls) {
|
|
445
|
+
for (const tc of response.toolCalls) {
|
|
446
|
+
yield { type: "tool_call", toolCall: tc };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
yield { type: "done" };
|
|
450
|
+
} catch (error) {
|
|
451
|
+
yield { type: "error", error: error.message };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async listModels() {
|
|
455
|
+
return [
|
|
456
|
+
"claude-opus-4-0",
|
|
457
|
+
"claude-sonnet-4-20250514",
|
|
458
|
+
"claude-haiku-4-20250414",
|
|
459
|
+
"claude-3-5-sonnet-20241022",
|
|
460
|
+
"claude-3-5-haiku-20241022"
|
|
461
|
+
];
|
|
462
|
+
}
|
|
463
|
+
async healthCheck() {
|
|
464
|
+
try {
|
|
465
|
+
if (!this.apiKey) return false;
|
|
466
|
+
const response = await fetch(`${this.baseUrl}/v1/messages`, {
|
|
467
|
+
method: "POST",
|
|
468
|
+
headers: {
|
|
469
|
+
"Content-Type": "application/json",
|
|
470
|
+
"x-api-key": this.apiKey,
|
|
471
|
+
"anthropic-version": "2023-06-01"
|
|
472
|
+
},
|
|
473
|
+
body: JSON.stringify({
|
|
474
|
+
model: "claude-haiku-4-20250414",
|
|
475
|
+
max_tokens: 1,
|
|
476
|
+
messages: [{ role: "user", content: "ping" }]
|
|
477
|
+
})
|
|
478
|
+
});
|
|
479
|
+
return response.ok || response.status === 400;
|
|
480
|
+
} catch {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// src/providers/openai.ts
|
|
487
|
+
init_logger();
|
|
488
|
+
import { v4 as uuid2 } from "uuid";
|
|
489
|
+
var COMPONENT3 = "OpenAI";
|
|
490
|
+
var OpenAIProvider = class extends LLMProvider {
|
|
491
|
+
name = "openai";
|
|
492
|
+
displayName = "OpenAI (GPT)";
|
|
493
|
+
get apiKey() {
|
|
494
|
+
const config = loadConfig();
|
|
495
|
+
return config.providers.openai.apiKey || process.env.OPENAI_API_KEY || "";
|
|
496
|
+
}
|
|
497
|
+
get baseUrl() {
|
|
498
|
+
const config = loadConfig();
|
|
499
|
+
return config.providers.openai.baseUrl || "https://api.openai.com";
|
|
500
|
+
}
|
|
501
|
+
async chat(options) {
|
|
502
|
+
const model = options.model || "gpt-4o";
|
|
503
|
+
const apiKey = this.apiKey;
|
|
504
|
+
if (!apiKey) throw new Error("OpenAI API key not configured");
|
|
505
|
+
logger_default.debug(COMPONENT3, `Chat request: model=${model}, messages=${options.messages.length}`);
|
|
506
|
+
const body = {
|
|
507
|
+
model: model.replace("openai/", ""),
|
|
508
|
+
messages: options.messages.map((m) => {
|
|
509
|
+
if (m.role === "tool") {
|
|
510
|
+
return { role: "tool", content: m.content, tool_call_id: m.toolCallId };
|
|
511
|
+
}
|
|
512
|
+
if (m.role === "assistant" && m.toolCalls) {
|
|
513
|
+
return {
|
|
514
|
+
role: "assistant",
|
|
515
|
+
content: m.content || null,
|
|
516
|
+
tool_calls: m.toolCalls.map((tc) => ({
|
|
517
|
+
id: tc.id,
|
|
518
|
+
type: "function",
|
|
519
|
+
function: { name: tc.function.name, arguments: tc.function.arguments }
|
|
520
|
+
}))
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
return { role: m.role, content: m.content };
|
|
524
|
+
}),
|
|
525
|
+
max_tokens: options.maxTokens || 8192
|
|
526
|
+
};
|
|
527
|
+
if (options.tools && options.tools.length > 0) {
|
|
528
|
+
body.tools = options.tools;
|
|
529
|
+
}
|
|
530
|
+
if (options.temperature !== void 0) {
|
|
531
|
+
body.temperature = options.temperature;
|
|
532
|
+
}
|
|
533
|
+
const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
534
|
+
method: "POST",
|
|
535
|
+
headers: {
|
|
536
|
+
"Content-Type": "application/json",
|
|
537
|
+
Authorization: `Bearer ${apiKey}`
|
|
538
|
+
},
|
|
539
|
+
body: JSON.stringify(body)
|
|
540
|
+
});
|
|
541
|
+
if (!response.ok) {
|
|
542
|
+
const errorText = await response.text();
|
|
543
|
+
throw new Error(`OpenAI API error (${response.status}): ${errorText}`);
|
|
544
|
+
}
|
|
545
|
+
const data = await response.json();
|
|
546
|
+
const choices = data.choices;
|
|
547
|
+
const choice = choices[0];
|
|
548
|
+
const message = choice.message;
|
|
549
|
+
const toolCalls = [];
|
|
550
|
+
if (message.tool_calls) {
|
|
551
|
+
for (const tc of message.tool_calls) {
|
|
552
|
+
const fn = tc.function;
|
|
553
|
+
toolCalls.push({
|
|
554
|
+
id: tc.id,
|
|
555
|
+
type: "function",
|
|
556
|
+
function: { name: fn.name, arguments: fn.arguments }
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const usage = data.usage;
|
|
561
|
+
return {
|
|
562
|
+
id: data.id || uuid2(),
|
|
563
|
+
content: message.content || "",
|
|
564
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
565
|
+
usage: usage ? {
|
|
566
|
+
promptTokens: usage.prompt_tokens,
|
|
567
|
+
completionTokens: usage.completion_tokens,
|
|
568
|
+
totalTokens: usage.total_tokens
|
|
569
|
+
} : void 0,
|
|
570
|
+
finishReason: toolCalls.length > 0 ? "tool_calls" : choice.finish_reason || "stop",
|
|
571
|
+
model
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
async *chatStream(options) {
|
|
575
|
+
try {
|
|
576
|
+
const response = await this.chat(options);
|
|
577
|
+
if (response.content) {
|
|
578
|
+
yield { type: "text", content: response.content };
|
|
579
|
+
}
|
|
580
|
+
if (response.toolCalls) {
|
|
581
|
+
for (const tc of response.toolCalls) {
|
|
582
|
+
yield { type: "tool_call", toolCall: tc };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
yield { type: "done" };
|
|
586
|
+
} catch (error) {
|
|
587
|
+
yield { type: "error", error: error.message };
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
async listModels() {
|
|
591
|
+
return ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "o1", "o1-mini", "o3-mini"];
|
|
592
|
+
}
|
|
593
|
+
async healthCheck() {
|
|
594
|
+
try {
|
|
595
|
+
if (!this.apiKey) return false;
|
|
596
|
+
const response = await fetch(`${this.baseUrl}/v1/models`, {
|
|
597
|
+
headers: { Authorization: `Bearer ${this.apiKey}` }
|
|
598
|
+
});
|
|
599
|
+
return response.ok;
|
|
600
|
+
} catch {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
// src/providers/google.ts
|
|
607
|
+
init_logger();
|
|
608
|
+
import { v4 as uuid3 } from "uuid";
|
|
609
|
+
var COMPONENT4 = "Google";
|
|
610
|
+
var GoogleProvider = class extends LLMProvider {
|
|
611
|
+
name = "google";
|
|
612
|
+
displayName = "Google (Gemini)";
|
|
613
|
+
get apiKey() {
|
|
614
|
+
const config = loadConfig();
|
|
615
|
+
return config.providers.google.apiKey || process.env.GOOGLE_API_KEY || "";
|
|
616
|
+
}
|
|
617
|
+
async chat(options) {
|
|
618
|
+
const model = (options.model || "gemini-2.0-flash").replace("google/", "");
|
|
619
|
+
const apiKey = this.apiKey;
|
|
620
|
+
if (!apiKey) throw new Error("Google API key not configured");
|
|
621
|
+
logger_default.debug(COMPONENT4, `Chat request: model=${model}, messages=${options.messages.length}`);
|
|
622
|
+
const systemInstruction = options.messages.find((m) => m.role === "system")?.content;
|
|
623
|
+
const contents = options.messages.filter((m) => m.role !== "system").map((m) => ({
|
|
624
|
+
role: m.role === "assistant" ? "model" : "user",
|
|
625
|
+
parts: [{ text: m.content }]
|
|
626
|
+
}));
|
|
627
|
+
const body = {
|
|
628
|
+
contents,
|
|
629
|
+
generationConfig: {
|
|
630
|
+
maxOutputTokens: options.maxTokens || 8192,
|
|
631
|
+
temperature: options.temperature ?? 0.7
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
if (systemInstruction) {
|
|
635
|
+
body.systemInstruction = { parts: [{ text: systemInstruction }] };
|
|
636
|
+
}
|
|
637
|
+
if (options.tools && options.tools.length > 0) {
|
|
638
|
+
body.tools = [{
|
|
639
|
+
functionDeclarations: options.tools.map((t) => ({
|
|
640
|
+
name: t.function.name,
|
|
641
|
+
description: t.function.description,
|
|
642
|
+
parameters: t.function.parameters
|
|
643
|
+
}))
|
|
644
|
+
}];
|
|
645
|
+
}
|
|
646
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
|
|
647
|
+
const response = await fetch(url, {
|
|
648
|
+
method: "POST",
|
|
649
|
+
headers: { "Content-Type": "application/json" },
|
|
650
|
+
body: JSON.stringify(body)
|
|
651
|
+
});
|
|
652
|
+
if (!response.ok) {
|
|
653
|
+
const errorText = await response.text();
|
|
654
|
+
throw new Error(`Google API error (${response.status}): ${errorText}`);
|
|
655
|
+
}
|
|
656
|
+
const data = await response.json();
|
|
657
|
+
const candidates = data.candidates;
|
|
658
|
+
let textContent = "";
|
|
659
|
+
const toolCalls = [];
|
|
660
|
+
if (candidates && candidates.length > 0) {
|
|
661
|
+
const parts = candidates[0].content?.parts || [];
|
|
662
|
+
for (const part of parts) {
|
|
663
|
+
if (part.text) {
|
|
664
|
+
textContent += part.text;
|
|
665
|
+
}
|
|
666
|
+
if (part.functionCall) {
|
|
667
|
+
const fc = part.functionCall;
|
|
668
|
+
toolCalls.push({
|
|
669
|
+
id: uuid3(),
|
|
670
|
+
type: "function",
|
|
671
|
+
function: {
|
|
672
|
+
name: fc.name,
|
|
673
|
+
arguments: JSON.stringify(fc.args)
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const usageMeta = data.usageMetadata;
|
|
680
|
+
return {
|
|
681
|
+
id: uuid3(),
|
|
682
|
+
content: textContent,
|
|
683
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
684
|
+
usage: usageMeta ? {
|
|
685
|
+
promptTokens: usageMeta.promptTokenCount || 0,
|
|
686
|
+
completionTokens: usageMeta.candidatesTokenCount || 0,
|
|
687
|
+
totalTokens: usageMeta.totalTokenCount || 0
|
|
688
|
+
} : void 0,
|
|
689
|
+
finishReason: toolCalls.length > 0 ? "tool_calls" : "stop",
|
|
690
|
+
model: `google/${model}`
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
async *chatStream(options) {
|
|
694
|
+
try {
|
|
695
|
+
const response = await this.chat(options);
|
|
696
|
+
if (response.content) yield { type: "text", content: response.content };
|
|
697
|
+
if (response.toolCalls) {
|
|
698
|
+
for (const tc of response.toolCalls) yield { type: "tool_call", toolCall: tc };
|
|
699
|
+
}
|
|
700
|
+
yield { type: "done" };
|
|
701
|
+
} catch (error) {
|
|
702
|
+
yield { type: "error", error: error.message };
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
async listModels() {
|
|
706
|
+
return ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash", "gemini-1.5-pro"];
|
|
707
|
+
}
|
|
708
|
+
async healthCheck() {
|
|
709
|
+
try {
|
|
710
|
+
if (!this.apiKey) return false;
|
|
711
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models?key=${this.apiKey}`;
|
|
712
|
+
const response = await fetch(url);
|
|
713
|
+
return response.ok;
|
|
714
|
+
} catch {
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
// src/providers/ollama.ts
|
|
721
|
+
init_logger();
|
|
722
|
+
import { v4 as uuid4 } from "uuid";
|
|
723
|
+
var COMPONENT5 = "Ollama";
|
|
724
|
+
var OllamaProvider = class extends LLMProvider {
|
|
725
|
+
name = "ollama";
|
|
726
|
+
displayName = "Ollama (Local)";
|
|
727
|
+
get baseUrl() {
|
|
728
|
+
const config = loadConfig();
|
|
729
|
+
return config.providers.ollama.baseUrl || process.env.OLLAMA_BASE_URL || "http://localhost:11434";
|
|
730
|
+
}
|
|
731
|
+
async chat(options) {
|
|
732
|
+
const model = (options.model || "llama3.1").replace("ollama/", "");
|
|
733
|
+
logger_default.debug(COMPONENT5, `Chat request: model=${model}, messages=${options.messages.length}`);
|
|
734
|
+
const body = {
|
|
735
|
+
model,
|
|
736
|
+
messages: options.messages.map((m) => ({
|
|
737
|
+
role: m.role,
|
|
738
|
+
content: m.content
|
|
739
|
+
})),
|
|
740
|
+
stream: false,
|
|
741
|
+
options: {
|
|
742
|
+
num_predict: options.maxTokens || 8192,
|
|
743
|
+
temperature: options.temperature ?? 0.7
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
if (options.tools && options.tools.length > 0) {
|
|
747
|
+
body.tools = options.tools.map((t) => ({
|
|
748
|
+
type: "function",
|
|
749
|
+
function: {
|
|
750
|
+
name: t.function.name,
|
|
751
|
+
description: t.function.description,
|
|
752
|
+
parameters: t.function.parameters
|
|
753
|
+
}
|
|
754
|
+
}));
|
|
755
|
+
}
|
|
756
|
+
const response = await fetch(`${this.baseUrl}/api/chat`, {
|
|
757
|
+
method: "POST",
|
|
758
|
+
headers: { "Content-Type": "application/json" },
|
|
759
|
+
body: JSON.stringify(body)
|
|
760
|
+
});
|
|
761
|
+
if (!response.ok) {
|
|
762
|
+
const errorText = await response.text();
|
|
763
|
+
throw new Error(`Ollama error (${response.status}): ${errorText}`);
|
|
764
|
+
}
|
|
765
|
+
const data = await response.json();
|
|
766
|
+
const message = data.message;
|
|
767
|
+
const toolCalls = [];
|
|
768
|
+
if (message.tool_calls) {
|
|
769
|
+
for (const tc of message.tool_calls) {
|
|
770
|
+
const fn = tc.function;
|
|
771
|
+
toolCalls.push({
|
|
772
|
+
id: uuid4(),
|
|
773
|
+
type: "function",
|
|
774
|
+
function: {
|
|
775
|
+
name: fn.name,
|
|
776
|
+
arguments: JSON.stringify(fn.arguments)
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return {
|
|
782
|
+
id: uuid4(),
|
|
783
|
+
content: message.content || "",
|
|
784
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
785
|
+
usage: {
|
|
786
|
+
promptTokens: data.prompt_eval_count || 0,
|
|
787
|
+
completionTokens: data.eval_count || 0,
|
|
788
|
+
totalTokens: (data.prompt_eval_count || 0) + (data.eval_count || 0)
|
|
789
|
+
},
|
|
790
|
+
finishReason: toolCalls.length > 0 ? "tool_calls" : "stop",
|
|
791
|
+
model: `ollama/${model}`
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
async *chatStream(options) {
|
|
795
|
+
try {
|
|
796
|
+
const response = await this.chat(options);
|
|
797
|
+
if (response.content) yield { type: "text", content: response.content };
|
|
798
|
+
if (response.toolCalls) {
|
|
799
|
+
for (const tc of response.toolCalls) yield { type: "tool_call", toolCall: tc };
|
|
800
|
+
}
|
|
801
|
+
yield { type: "done" };
|
|
802
|
+
} catch (error) {
|
|
803
|
+
yield { type: "error", error: error.message };
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
async listModels() {
|
|
807
|
+
try {
|
|
808
|
+
const response = await fetch(`${this.baseUrl}/api/tags`);
|
|
809
|
+
if (!response.ok) return [];
|
|
810
|
+
const data = await response.json();
|
|
811
|
+
return (data.models || []).map((m) => m.name);
|
|
812
|
+
} catch {
|
|
813
|
+
return [];
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
async healthCheck() {
|
|
817
|
+
try {
|
|
818
|
+
const response = await fetch(`${this.baseUrl}/api/tags`);
|
|
819
|
+
return response.ok;
|
|
820
|
+
} catch {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
// src/providers/router.ts
|
|
827
|
+
init_logger();
|
|
828
|
+
var COMPONENT6 = "Router";
|
|
829
|
+
var providers = /* @__PURE__ */ new Map();
|
|
830
|
+
var initialized = false;
|
|
831
|
+
function initProviders() {
|
|
832
|
+
if (initialized) return;
|
|
833
|
+
providers.set("anthropic", new AnthropicProvider());
|
|
834
|
+
providers.set("openai", new OpenAIProvider());
|
|
835
|
+
providers.set("google", new GoogleProvider());
|
|
836
|
+
providers.set("ollama", new OllamaProvider());
|
|
837
|
+
initialized = true;
|
|
838
|
+
}
|
|
839
|
+
function resolveModel(modelId) {
|
|
840
|
+
initProviders();
|
|
841
|
+
const { provider: providerName, model } = LLMProvider.parseModelId(modelId);
|
|
842
|
+
const provider = providers.get(providerName);
|
|
843
|
+
if (!provider) {
|
|
844
|
+
throw new Error(`Unknown provider: ${providerName}. Available: ${Array.from(providers.keys()).join(", ")}`);
|
|
845
|
+
}
|
|
846
|
+
return { provider, model };
|
|
847
|
+
}
|
|
848
|
+
async function chat(options) {
|
|
849
|
+
const modelId = options.model || "anthropic/claude-sonnet-4-20250514";
|
|
850
|
+
const { provider, model } = resolveModel(modelId);
|
|
851
|
+
logger_default.info(COMPONENT6, `Routing to ${provider.displayName} (model: ${model})`);
|
|
852
|
+
try {
|
|
853
|
+
return await provider.chat({ ...options, model });
|
|
854
|
+
} catch (error) {
|
|
855
|
+
logger_default.error(COMPONENT6, `Provider ${provider.name} failed: ${error.message}`);
|
|
856
|
+
const failoverOrder = ["anthropic", "openai", "google", "ollama"];
|
|
857
|
+
for (const fallbackName of failoverOrder) {
|
|
858
|
+
if (fallbackName === provider.name) continue;
|
|
859
|
+
const fallback = providers.get(fallbackName);
|
|
860
|
+
if (!fallback) continue;
|
|
861
|
+
try {
|
|
862
|
+
const healthy = await fallback.healthCheck();
|
|
863
|
+
if (!healthy) continue;
|
|
864
|
+
const models = await fallback.listModels();
|
|
865
|
+
if (models.length === 0) continue;
|
|
866
|
+
logger_default.warn(COMPONENT6, `Failing over to ${fallback.displayName} (model: ${models[0]})`);
|
|
867
|
+
return await fallback.chat({ ...options, model: models[0] });
|
|
868
|
+
} catch {
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
throw error;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/agent/session.ts
|
|
877
|
+
import { v4 as uuid5 } from "uuid";
|
|
878
|
+
|
|
879
|
+
// src/memory/memory.ts
|
|
880
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
881
|
+
import { join as join2 } from "path";
|
|
882
|
+
init_logger();
|
|
883
|
+
init_encryption();
|
|
884
|
+
var COMPONENT8 = "Memory";
|
|
885
|
+
var DB_FILE = join2(TITAN_HOME, "titan-data.json");
|
|
886
|
+
var store = null;
|
|
887
|
+
function getDefaultStore() {
|
|
888
|
+
return {
|
|
889
|
+
conversations: [],
|
|
890
|
+
memories: [],
|
|
891
|
+
sessions: [],
|
|
892
|
+
usageStats: [],
|
|
893
|
+
cronJobs: [],
|
|
894
|
+
skillsInstalled: []
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
function loadStore() {
|
|
898
|
+
if (store) return store;
|
|
899
|
+
ensureDir(TITAN_HOME);
|
|
900
|
+
if (existsSync3(DB_FILE)) {
|
|
901
|
+
try {
|
|
902
|
+
const raw = readFileSync2(DB_FILE, "utf-8");
|
|
903
|
+
store = JSON.parse(raw);
|
|
904
|
+
store.conversations = store.conversations || [];
|
|
905
|
+
store.memories = store.memories || [];
|
|
906
|
+
store.sessions = store.sessions || [];
|
|
907
|
+
store.usageStats = store.usageStats || [];
|
|
908
|
+
store.cronJobs = store.cronJobs || [];
|
|
909
|
+
store.skillsInstalled = store.skillsInstalled || [];
|
|
910
|
+
} catch {
|
|
911
|
+
logger_default.warn(COMPONENT8, "Could not load data store, creating fresh one");
|
|
912
|
+
store = getDefaultStore();
|
|
913
|
+
}
|
|
914
|
+
} else {
|
|
915
|
+
store = getDefaultStore();
|
|
916
|
+
}
|
|
917
|
+
return store;
|
|
918
|
+
}
|
|
919
|
+
function saveStore() {
|
|
920
|
+
if (!store) return;
|
|
921
|
+
ensureDir(TITAN_HOME);
|
|
922
|
+
try {
|
|
923
|
+
writeFileSync2(DB_FILE, JSON.stringify(store, null, 2), "utf-8");
|
|
924
|
+
} catch (e) {
|
|
925
|
+
logger_default.error(COMPONENT8, `Failed to save data: ${e.message}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
var saveTimeout = null;
|
|
929
|
+
function debouncedSave() {
|
|
930
|
+
if (saveTimeout) clearTimeout(saveTimeout);
|
|
931
|
+
saveTimeout = setTimeout(saveStore, 1e3);
|
|
932
|
+
}
|
|
933
|
+
function getDb() {
|
|
934
|
+
return loadStore();
|
|
935
|
+
}
|
|
936
|
+
function saveMessage(message, e2eKey) {
|
|
937
|
+
const s = loadStore();
|
|
938
|
+
let content = message.content;
|
|
939
|
+
let isEncrypted = false;
|
|
940
|
+
if (e2eKey) {
|
|
941
|
+
try {
|
|
942
|
+
const payload = encrypt(message.content, Buffer.from(e2eKey, "base64"));
|
|
943
|
+
content = JSON.stringify(payload);
|
|
944
|
+
isEncrypted = true;
|
|
945
|
+
} catch (e) {
|
|
946
|
+
logger_default.error(COMPONENT8, `Failed to encrypt message for storage`);
|
|
947
|
+
content = "[ENCRYPTION FAILED] " + content;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
s.conversations.push({
|
|
951
|
+
...message,
|
|
952
|
+
content,
|
|
953
|
+
isEncrypted,
|
|
954
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
955
|
+
});
|
|
956
|
+
if (s.conversations.length > 5e3) {
|
|
957
|
+
s.conversations = s.conversations.slice(-5e3);
|
|
958
|
+
}
|
|
959
|
+
debouncedSave();
|
|
960
|
+
}
|
|
961
|
+
function getHistory(sessionId, limit = 50, e2eKey) {
|
|
962
|
+
const s = loadStore();
|
|
963
|
+
const rawHistory = s.conversations.filter((m) => m.sessionId === sessionId).slice(-limit);
|
|
964
|
+
if (!e2eKey) {
|
|
965
|
+
return rawHistory;
|
|
966
|
+
}
|
|
967
|
+
return rawHistory.map((m) => {
|
|
968
|
+
if (m.isEncrypted) {
|
|
969
|
+
try {
|
|
970
|
+
const payload = JSON.parse(m.content);
|
|
971
|
+
return {
|
|
972
|
+
...m,
|
|
973
|
+
content: decrypt(payload, Buffer.from(e2eKey, "base64"))
|
|
974
|
+
};
|
|
975
|
+
} catch (e) {
|
|
976
|
+
logger_default.error(COMPONENT8, `Failed to decrypt message ${m.id}`);
|
|
977
|
+
return { ...m, content: "[DECRYPTION FAILED]" };
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return m;
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
function searchMemories(category, query) {
|
|
984
|
+
const s = loadStore();
|
|
985
|
+
let results = s.memories;
|
|
986
|
+
if (category) {
|
|
987
|
+
results = results.filter((m) => m.category === category);
|
|
988
|
+
}
|
|
989
|
+
if (query) {
|
|
990
|
+
const q = query.toLowerCase();
|
|
991
|
+
results = results.filter(
|
|
992
|
+
(m) => m.key.toLowerCase().includes(q) || m.value.toLowerCase().includes(q)
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
return results.slice(-50).map((m) => ({ key: m.key, value: m.value, category: m.category }));
|
|
996
|
+
}
|
|
997
|
+
function recordUsage(sessionId, provider, model, promptTokens, completionTokens) {
|
|
998
|
+
const s = loadStore();
|
|
999
|
+
s.usageStats.push({
|
|
1000
|
+
id: s.usageStats.length + 1,
|
|
1001
|
+
session_id: sessionId,
|
|
1002
|
+
provider,
|
|
1003
|
+
model,
|
|
1004
|
+
prompt_tokens: promptTokens,
|
|
1005
|
+
completion_tokens: completionTokens,
|
|
1006
|
+
total_tokens: promptTokens + completionTokens,
|
|
1007
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1008
|
+
});
|
|
1009
|
+
if (s.usageStats.length > 1e4) {
|
|
1010
|
+
s.usageStats = s.usageStats.slice(-1e4);
|
|
1011
|
+
}
|
|
1012
|
+
debouncedSave();
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// src/agent/session.ts
|
|
1016
|
+
init_logger();
|
|
1017
|
+
var COMPONENT9 = "Session";
|
|
1018
|
+
var activeSessions = /* @__PURE__ */ new Map();
|
|
1019
|
+
function getOrCreateSession(channel, userId, agentId = "default", isEncrypted = false) {
|
|
1020
|
+
const sessionKey = `${channel}:${userId}:${agentId}`;
|
|
1021
|
+
const cached = activeSessions.get(sessionKey);
|
|
1022
|
+
if (cached && cached.status === "active") {
|
|
1023
|
+
return cached;
|
|
1024
|
+
}
|
|
1025
|
+
const store2 = getDb();
|
|
1026
|
+
const existing = store2.sessions.find(
|
|
1027
|
+
(s) => s.channel === channel && s.user_id === userId && s.agent_id === agentId && s.status === "active"
|
|
1028
|
+
);
|
|
1029
|
+
if (existing) {
|
|
1030
|
+
const lastActive = new Date(existing.last_active || existing.created_at).getTime();
|
|
1031
|
+
if (Date.now() - lastActive > SESSION_TIMEOUT_MS) {
|
|
1032
|
+
existing.status = "idle";
|
|
1033
|
+
logger_default.debug(COMPONENT9, `Session ${existing.id} timed out, creating new one`);
|
|
1034
|
+
} else {
|
|
1035
|
+
const session2 = {
|
|
1036
|
+
id: existing.id,
|
|
1037
|
+
channel: existing.channel,
|
|
1038
|
+
userId: existing.user_id,
|
|
1039
|
+
agentId: existing.agent_id,
|
|
1040
|
+
status: existing.status,
|
|
1041
|
+
messageCount: existing.message_count,
|
|
1042
|
+
createdAt: existing.created_at,
|
|
1043
|
+
lastActive: existing.last_active,
|
|
1044
|
+
// Note: If a session was encrypted but dropped from memory, we cannot recover the key
|
|
1045
|
+
// A robust implementation would involve key exchange, but for now we warn:
|
|
1046
|
+
e2eKey: void 0
|
|
1047
|
+
};
|
|
1048
|
+
if (isEncrypted) {
|
|
1049
|
+
logger_default.warn(COMPONENT9, `Recovered session ${existing.id}, but E2E key was lost from memory.`);
|
|
1050
|
+
}
|
|
1051
|
+
activeSessions.set(sessionKey, session2);
|
|
1052
|
+
return session2;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const session = {
|
|
1056
|
+
id: uuid5(),
|
|
1057
|
+
channel,
|
|
1058
|
+
userId,
|
|
1059
|
+
agentId,
|
|
1060
|
+
status: "active",
|
|
1061
|
+
messageCount: 0,
|
|
1062
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1063
|
+
lastActive: (/* @__PURE__ */ new Date()).toISOString()
|
|
1064
|
+
};
|
|
1065
|
+
if (isEncrypted) {
|
|
1066
|
+
Promise.resolve().then(() => (init_encryption(), encryption_exports)).then(({ generateKey: generateKey2 }) => {
|
|
1067
|
+
session.e2eKey = generateKey2().toString("base64");
|
|
1068
|
+
logger_default.info(COMPONENT9, `Generated E2E key for session ${session.id}`);
|
|
1069
|
+
}).catch((err) => {
|
|
1070
|
+
logger_default.error(COMPONENT9, `Failed to load encryption module: ${err}`);
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
store2.sessions.push({
|
|
1074
|
+
id: session.id,
|
|
1075
|
+
channel,
|
|
1076
|
+
user_id: userId,
|
|
1077
|
+
agent_id: agentId,
|
|
1078
|
+
status: "active",
|
|
1079
|
+
message_count: 0,
|
|
1080
|
+
created_at: session.createdAt,
|
|
1081
|
+
last_active: session.lastActive
|
|
1082
|
+
});
|
|
1083
|
+
activeSessions.set(sessionKey, session);
|
|
1084
|
+
logger_default.info(COMPONENT9, `Created new session: ${session.id} (${channel}/${userId})`);
|
|
1085
|
+
return session;
|
|
1086
|
+
}
|
|
1087
|
+
function addMessage(session, role, content, extra) {
|
|
1088
|
+
const messageId = uuid5();
|
|
1089
|
+
saveMessage({
|
|
1090
|
+
id: messageId,
|
|
1091
|
+
sessionId: session.id,
|
|
1092
|
+
role,
|
|
1093
|
+
content,
|
|
1094
|
+
toolCalls: extra?.toolCalls,
|
|
1095
|
+
toolCallId: extra?.toolCallId,
|
|
1096
|
+
model: extra?.model,
|
|
1097
|
+
tokenCount: extra?.tokenCount || 0
|
|
1098
|
+
}, session.e2eKey);
|
|
1099
|
+
session.messageCount++;
|
|
1100
|
+
session.lastActive = (/* @__PURE__ */ new Date()).toISOString();
|
|
1101
|
+
const store2 = getDb();
|
|
1102
|
+
const sessionRec = store2.sessions.find((s) => s.id === session.id);
|
|
1103
|
+
if (sessionRec) {
|
|
1104
|
+
sessionRec.message_count = session.messageCount;
|
|
1105
|
+
sessionRec.last_active = session.lastActive;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function getContextMessages(session, maxMessages = MAX_CONTEXT_MESSAGES) {
|
|
1109
|
+
const history = getHistory(session.id, maxMessages, session.e2eKey);
|
|
1110
|
+
return history.map((msg) => ({
|
|
1111
|
+
role: msg.role,
|
|
1112
|
+
content: msg.content,
|
|
1113
|
+
toolCallId: msg.toolCallId || void 0,
|
|
1114
|
+
toolCalls: msg.toolCalls ? JSON.parse(msg.toolCalls) : void 0
|
|
1115
|
+
}));
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// src/agent/toolRunner.ts
|
|
1119
|
+
init_logger();
|
|
1120
|
+
var COMPONENT10 = "ToolRunner";
|
|
1121
|
+
var toolRegistry = /* @__PURE__ */ new Map();
|
|
1122
|
+
function getToolDefinitions() {
|
|
1123
|
+
const config = loadConfig();
|
|
1124
|
+
const allowed = new Set(config.security.allowedTools);
|
|
1125
|
+
const denied = new Set(config.security.deniedTools);
|
|
1126
|
+
return Array.from(toolRegistry.values()).filter((tool) => {
|
|
1127
|
+
if (denied.has(tool.name)) return false;
|
|
1128
|
+
if (allowed.size > 0 && !allowed.has(tool.name)) return false;
|
|
1129
|
+
return true;
|
|
1130
|
+
}).map((tool) => ({
|
|
1131
|
+
type: "function",
|
|
1132
|
+
function: {
|
|
1133
|
+
name: tool.name,
|
|
1134
|
+
description: tool.description,
|
|
1135
|
+
parameters: tool.parameters
|
|
1136
|
+
}
|
|
1137
|
+
}));
|
|
1138
|
+
}
|
|
1139
|
+
async function executeTool(toolCall) {
|
|
1140
|
+
const config = loadConfig();
|
|
1141
|
+
const startTime = Date.now();
|
|
1142
|
+
const handler = toolRegistry.get(toolCall.function.name);
|
|
1143
|
+
if (!handler) {
|
|
1144
|
+
return {
|
|
1145
|
+
toolCallId: toolCall.id,
|
|
1146
|
+
name: toolCall.function.name,
|
|
1147
|
+
content: `Error: Unknown tool "${toolCall.function.name}"`,
|
|
1148
|
+
success: false,
|
|
1149
|
+
durationMs: Date.now() - startTime
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
if (config.security.deniedTools.includes(handler.name)) {
|
|
1153
|
+
return {
|
|
1154
|
+
toolCallId: toolCall.id,
|
|
1155
|
+
name: handler.name,
|
|
1156
|
+
content: `Error: Tool "${handler.name}" is denied by security policy`,
|
|
1157
|
+
success: false,
|
|
1158
|
+
durationMs: Date.now() - startTime
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
try {
|
|
1162
|
+
let args = {};
|
|
1163
|
+
try {
|
|
1164
|
+
args = JSON.parse(toolCall.function.arguments);
|
|
1165
|
+
} catch {
|
|
1166
|
+
args = {};
|
|
1167
|
+
}
|
|
1168
|
+
logger_default.info(COMPONENT10, `Executing tool: ${handler.name}`);
|
|
1169
|
+
const timeout = config.security.commandTimeout || 3e4;
|
|
1170
|
+
const result = await Promise.race([
|
|
1171
|
+
handler.execute(args),
|
|
1172
|
+
new Promise(
|
|
1173
|
+
(_, reject) => setTimeout(() => reject(new Error(`Tool "${handler.name}" timed out after ${timeout}ms`)), timeout)
|
|
1174
|
+
)
|
|
1175
|
+
]);
|
|
1176
|
+
const durationMs = Date.now() - startTime;
|
|
1177
|
+
logger_default.info(COMPONENT10, `Tool ${handler.name} completed in ${durationMs}ms`);
|
|
1178
|
+
return {
|
|
1179
|
+
toolCallId: toolCall.id,
|
|
1180
|
+
name: handler.name,
|
|
1181
|
+
content: result,
|
|
1182
|
+
success: true,
|
|
1183
|
+
durationMs
|
|
1184
|
+
};
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
const durationMs = Date.now() - startTime;
|
|
1187
|
+
const errorMsg = error.message;
|
|
1188
|
+
logger_default.error(COMPONENT10, `Tool ${handler.name} failed: ${errorMsg}`);
|
|
1189
|
+
return {
|
|
1190
|
+
toolCallId: toolCall.id,
|
|
1191
|
+
name: handler.name,
|
|
1192
|
+
content: `Error: ${errorMsg}`,
|
|
1193
|
+
success: false,
|
|
1194
|
+
durationMs
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
async function executeTools(toolCalls) {
|
|
1199
|
+
const config = loadConfig();
|
|
1200
|
+
const maxConcurrent = config.security.maxConcurrentTasks || 5;
|
|
1201
|
+
const results = [];
|
|
1202
|
+
for (let i = 0; i < toolCalls.length; i += maxConcurrent) {
|
|
1203
|
+
const batch = toolCalls.slice(i, i + maxConcurrent);
|
|
1204
|
+
const batchResults = await Promise.all(batch.map(executeTool));
|
|
1205
|
+
results.push(...batchResults);
|
|
1206
|
+
}
|
|
1207
|
+
return results;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// src/memory/learning.ts
|
|
1211
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1212
|
+
import { join as join3 } from "path";
|
|
1213
|
+
init_logger();
|
|
1214
|
+
var KNOWLEDGE_FILE = join3(TITAN_HOME, "knowledge.json");
|
|
1215
|
+
var kb = null;
|
|
1216
|
+
function loadKnowledgeBase() {
|
|
1217
|
+
if (kb) return kb;
|
|
1218
|
+
ensureDir(TITAN_HOME);
|
|
1219
|
+
if (existsSync4(KNOWLEDGE_FILE)) {
|
|
1220
|
+
try {
|
|
1221
|
+
kb = JSON.parse(readFileSync3(KNOWLEDGE_FILE, "utf-8"));
|
|
1222
|
+
kb.entries = kb.entries || [];
|
|
1223
|
+
kb.toolSuccessRates = kb.toolSuccessRates || {};
|
|
1224
|
+
kb.errorPatterns = kb.errorPatterns || {};
|
|
1225
|
+
kb.userCorrections = kb.userCorrections || [];
|
|
1226
|
+
kb.conversationInsights = kb.conversationInsights || [];
|
|
1227
|
+
} catch {
|
|
1228
|
+
kb = createEmptyKB();
|
|
1229
|
+
}
|
|
1230
|
+
} else {
|
|
1231
|
+
kb = createEmptyKB();
|
|
1232
|
+
}
|
|
1233
|
+
return kb;
|
|
1234
|
+
}
|
|
1235
|
+
function createEmptyKB() {
|
|
1236
|
+
return {
|
|
1237
|
+
entries: [],
|
|
1238
|
+
toolSuccessRates: {},
|
|
1239
|
+
errorPatterns: {},
|
|
1240
|
+
userCorrections: [],
|
|
1241
|
+
conversationInsights: []
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
var saveTimeout2 = null;
|
|
1245
|
+
function debouncedSave2() {
|
|
1246
|
+
if (saveTimeout2) clearTimeout(saveTimeout2);
|
|
1247
|
+
saveTimeout2 = setTimeout(() => {
|
|
1248
|
+
if (!kb) return;
|
|
1249
|
+
ensureDir(TITAN_HOME);
|
|
1250
|
+
writeFileSync3(KNOWLEDGE_FILE, JSON.stringify(kb, null, 2), "utf-8");
|
|
1251
|
+
}, 2e3);
|
|
1252
|
+
}
|
|
1253
|
+
function recordToolResult(toolName, success, context, error) {
|
|
1254
|
+
const k = loadKnowledgeBase();
|
|
1255
|
+
if (!k.toolSuccessRates[toolName]) {
|
|
1256
|
+
k.toolSuccessRates[toolName] = { success: 0, fail: 0, total: 0 };
|
|
1257
|
+
}
|
|
1258
|
+
k.toolSuccessRates[toolName].total++;
|
|
1259
|
+
if (success) {
|
|
1260
|
+
k.toolSuccessRates[toolName].success++;
|
|
1261
|
+
} else {
|
|
1262
|
+
k.toolSuccessRates[toolName].fail++;
|
|
1263
|
+
if (error) {
|
|
1264
|
+
const pattern = error.slice(0, 200);
|
|
1265
|
+
if (!k.errorPatterns[pattern]) {
|
|
1266
|
+
k.errorPatterns[pattern] = { count: 0, lastSeen: "" };
|
|
1267
|
+
}
|
|
1268
|
+
k.errorPatterns[pattern].count++;
|
|
1269
|
+
k.errorPatterns[pattern].lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
debouncedSave2();
|
|
1273
|
+
}
|
|
1274
|
+
function getToolRecommendations() {
|
|
1275
|
+
const k = loadKnowledgeBase();
|
|
1276
|
+
const recommendations = {};
|
|
1277
|
+
for (const [tool, stats] of Object.entries(k.toolSuccessRates)) {
|
|
1278
|
+
if (stats.total > 0) {
|
|
1279
|
+
recommendations[tool] = stats.success / stats.total;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return recommendations;
|
|
1283
|
+
}
|
|
1284
|
+
function getLearningContext() {
|
|
1285
|
+
const k = loadKnowledgeBase();
|
|
1286
|
+
const parts = [];
|
|
1287
|
+
const topEntries = k.entries.filter((e) => e.score > 0.6).sort((a, b) => b.score - a.score).slice(0, 10);
|
|
1288
|
+
if (topEntries.length > 0) {
|
|
1289
|
+
parts.push("Key learned facts:");
|
|
1290
|
+
for (const e of topEntries) {
|
|
1291
|
+
parts.push(`- [${e.category}] ${e.content}`);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
const toolRecs = getToolRecommendations();
|
|
1295
|
+
const bestTools = Object.entries(toolRecs).filter(([_, rate]) => rate > 0.8).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
1296
|
+
if (bestTools.length > 0) {
|
|
1297
|
+
parts.push("\nMost reliable tools:");
|
|
1298
|
+
for (const [tool, rate] of bestTools) {
|
|
1299
|
+
parts.push(`- ${tool}: ${Math.round(rate * 100)}% success rate`);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
const frequentErrors = Object.entries(k.errorPatterns).filter(([_, info]) => info.count > 2).sort((a, b) => b[1].count - a[1].count).slice(0, 3);
|
|
1303
|
+
if (frequentErrors.length > 0) {
|
|
1304
|
+
parts.push("\nCommon errors to avoid:");
|
|
1305
|
+
for (const [pattern, info] of frequentErrors) {
|
|
1306
|
+
parts.push(`- ${pattern.slice(0, 100)} (seen ${info.count}x)${info.resolution ? ` \u2192 Fix: ${info.resolution}` : ""}`);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
return parts.join("\n");
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// src/agent/agent.ts
|
|
1313
|
+
init_logger();
|
|
1314
|
+
var COMPONENT11 = "Agent";
|
|
1315
|
+
var MAX_TOOL_ROUNDS = 10;
|
|
1316
|
+
function readPromptFile(path) {
|
|
1317
|
+
try {
|
|
1318
|
+
if (existsSync5(path)) return readFileSync4(path, "utf-8");
|
|
1319
|
+
} catch {
|
|
1320
|
+
}
|
|
1321
|
+
return "";
|
|
1322
|
+
}
|
|
1323
|
+
function buildSystemPrompt(config) {
|
|
1324
|
+
const customPrompt = config.agent.systemPrompt || "";
|
|
1325
|
+
const memories = searchMemories("preference");
|
|
1326
|
+
const memoryContext = memories.length > 0 ? `
|
|
1327
|
+
|
|
1328
|
+
User preferences I remember:
|
|
1329
|
+
${memories.map((m) => `- ${m.key}: ${m.value}`).join("\n")}` : "";
|
|
1330
|
+
const agentsMd = readPromptFile(AGENTS_MD);
|
|
1331
|
+
const soulMd = readPromptFile(SOUL_MD);
|
|
1332
|
+
const toolsMd = readPromptFile(TOOLS_MD);
|
|
1333
|
+
const workspaceContext = [
|
|
1334
|
+
agentsMd ? `
|
|
1335
|
+
## Agent Instructions (AGENTS.md)
|
|
1336
|
+
${agentsMd}` : "",
|
|
1337
|
+
soulMd ? `
|
|
1338
|
+
## Personality (SOUL.md)
|
|
1339
|
+
${soulMd}` : "",
|
|
1340
|
+
toolsMd ? `
|
|
1341
|
+
## Tool Notes (TOOLS.md)
|
|
1342
|
+
${toolsMd}` : ""
|
|
1343
|
+
].filter(Boolean).join("\n");
|
|
1344
|
+
const learningContext = getLearningContext();
|
|
1345
|
+
return `You are ${TITAN_NAME}, The Intelligent Task Automation Network \u2014 a powerful personal AI assistant.
|
|
1346
|
+
|
|
1347
|
+
## Core Capabilities
|
|
1348
|
+
- Execute shell commands and scripts on the user's system
|
|
1349
|
+
- Read, write, edit, and manage files
|
|
1350
|
+
- Browse the web and extract information (browser control via CDP)
|
|
1351
|
+
- Schedule automated tasks with cron
|
|
1352
|
+
- Set up webhook endpoints
|
|
1353
|
+
- Search the web for current information
|
|
1354
|
+
- Control browser sessions (navigate, snapshot, evaluate)
|
|
1355
|
+
- Manage agent sessions (list, history, send, close)
|
|
1356
|
+
- Remember facts and user preferences persistently
|
|
1357
|
+
|
|
1358
|
+
## Behavior Guidelines
|
|
1359
|
+
- Be proactive: if a task implies follow-up actions, suggest or perform them
|
|
1360
|
+
- Be concise but thorough in responses
|
|
1361
|
+
- When executing commands, always explain what you're doing and why
|
|
1362
|
+
- If a task could be destructive (deleting files, etc.), confirm with the user first
|
|
1363
|
+
- Use tools when they would be helpful \u2014 don't just describe what could be done
|
|
1364
|
+
- Remember important information about the user for future conversations
|
|
1365
|
+
- If you encounter an error, try alternative approaches before reporting failure
|
|
1366
|
+
|
|
1367
|
+
## Security
|
|
1368
|
+
- Never expose API keys, passwords, or other secrets
|
|
1369
|
+
- Don't execute commands that could compromise system security without explicit approval
|
|
1370
|
+
- Respect file system boundaries set in the configuration
|
|
1371
|
+
|
|
1372
|
+
## Continuous Learning
|
|
1373
|
+
You get smarter with every interaction. Below is your accumulated knowledge:
|
|
1374
|
+
${learningContext}
|
|
1375
|
+
${customPrompt ? `
|
|
1376
|
+
## Custom Instructions
|
|
1377
|
+
${customPrompt}` : ""}${workspaceContext}${memoryContext}`;
|
|
1378
|
+
}
|
|
1379
|
+
async function processMessage(message, channel = "cli", userId = "default") {
|
|
1380
|
+
const startTime = Date.now();
|
|
1381
|
+
const config = loadConfig();
|
|
1382
|
+
const session = getOrCreateSession(channel, userId);
|
|
1383
|
+
logger_default.info(COMPONENT11, `Processing message in session ${session.id} (${channel}/${userId})`);
|
|
1384
|
+
addMessage(session, "user", message);
|
|
1385
|
+
const systemPrompt = buildSystemPrompt(config);
|
|
1386
|
+
const historyMessages = getContextMessages(session);
|
|
1387
|
+
const tools = getToolDefinitions();
|
|
1388
|
+
const messages = [
|
|
1389
|
+
{ role: "system", content: systemPrompt },
|
|
1390
|
+
...historyMessages
|
|
1391
|
+
];
|
|
1392
|
+
let totalPromptTokens = 0;
|
|
1393
|
+
let totalCompletionTokens = 0;
|
|
1394
|
+
const toolsUsed = [];
|
|
1395
|
+
let finalContent = "";
|
|
1396
|
+
let modelUsed = config.agent.model;
|
|
1397
|
+
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
|
1398
|
+
logger_default.debug(COMPONENT11, `Round ${round + 1}: ${messages.length} messages, ${tools.length} tools`);
|
|
1399
|
+
const response = await chat({
|
|
1400
|
+
model: config.agent.model,
|
|
1401
|
+
messages,
|
|
1402
|
+
tools: tools.length > 0 ? tools : void 0,
|
|
1403
|
+
maxTokens: config.agent.maxTokens,
|
|
1404
|
+
temperature: config.agent.temperature
|
|
1405
|
+
});
|
|
1406
|
+
modelUsed = response.model;
|
|
1407
|
+
totalPromptTokens += response.usage?.promptTokens || 0;
|
|
1408
|
+
totalCompletionTokens += response.usage?.completionTokens || 0;
|
|
1409
|
+
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
1410
|
+
finalContent = response.content;
|
|
1411
|
+
break;
|
|
1412
|
+
}
|
|
1413
|
+
logger_default.info(COMPONENT11, `LLM requested ${response.toolCalls.length} tool call(s)`);
|
|
1414
|
+
messages.push({
|
|
1415
|
+
role: "assistant",
|
|
1416
|
+
content: response.content || "",
|
|
1417
|
+
toolCalls: response.toolCalls
|
|
1418
|
+
});
|
|
1419
|
+
const toolResults = await executeTools(response.toolCalls);
|
|
1420
|
+
for (const result of toolResults) {
|
|
1421
|
+
toolsUsed.push(result.name);
|
|
1422
|
+
messages.push({
|
|
1423
|
+
role: "tool",
|
|
1424
|
+
content: result.content,
|
|
1425
|
+
toolCallId: result.toolCallId
|
|
1426
|
+
});
|
|
1427
|
+
const success = !result.content.toLowerCase().includes("error:");
|
|
1428
|
+
recordToolResult(result.name, success, void 0, success ? void 0 : result.content.slice(0, 200));
|
|
1429
|
+
}
|
|
1430
|
+
if (round === MAX_TOOL_ROUNDS - 1) {
|
|
1431
|
+
finalContent = response.content || "I completed the tool operations. Let me know if you need anything else.";
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
addMessage(session, "assistant", finalContent, {
|
|
1435
|
+
model: modelUsed,
|
|
1436
|
+
tokenCount: totalCompletionTokens
|
|
1437
|
+
});
|
|
1438
|
+
const { provider: providerName } = { provider: modelUsed.split("/")[0] || "unknown" };
|
|
1439
|
+
recordUsage(session.id, providerName, modelUsed, totalPromptTokens, totalCompletionTokens);
|
|
1440
|
+
const durationMs = Date.now() - startTime;
|
|
1441
|
+
logger_default.info(COMPONENT11, `Response generated in ${durationMs}ms (${totalPromptTokens + totalCompletionTokens} tokens)`);
|
|
1442
|
+
return {
|
|
1443
|
+
content: finalContent,
|
|
1444
|
+
sessionId: session.id,
|
|
1445
|
+
toolsUsed: [...new Set(toolsUsed)],
|
|
1446
|
+
tokenUsage: {
|
|
1447
|
+
prompt: totalPromptTokens,
|
|
1448
|
+
completion: totalCompletionTokens,
|
|
1449
|
+
total: totalPromptTokens + totalCompletionTokens
|
|
1450
|
+
},
|
|
1451
|
+
model: modelUsed,
|
|
1452
|
+
durationMs
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
export {
|
|
1456
|
+
processMessage
|
|
1457
|
+
};
|
|
1458
|
+
//# sourceMappingURL=agent.js.map
|