@arthovlabs/liber 0.0.0-stage → 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 +21 -0
- package/README.md +348 -2
- package/bin/liber +706 -0
- package/bin/liber-tui +244 -0
- package/bin/liber-tui.py +825 -0
- package/bin/liberd-entry +17 -0
- package/liber.yaml +84 -0
- package/package.json +43 -4
- package/resources/icons/appIcon.png +0 -0
- package/resources/icons/appIcon.svg +20 -0
- package/resources/icons/appIcon@512.png +0 -0
- package/resources/icons/atom-transparent-256.png +0 -0
- package/resources/icons/atom-transparent.png +0 -0
- package/resources/icons/atom-transparent.svg +19 -0
- package/resources/icons/make-atom.js +168 -0
- package/src/cli/ui.js +520 -0
- package/src/daemon/core/auth.js +217 -0
- package/src/daemon/core/automations.js +130 -0
- package/src/daemon/core/budget.js +145 -0
- package/src/daemon/core/circuit-breaker.js +31 -0
- package/src/daemon/core/config-apply.js +223 -0
- package/src/daemon/core/connectors.js +131 -0
- package/src/daemon/core/engine.js +1029 -0
- package/src/daemon/core/evidence.js +306 -0
- package/src/daemon/core/executor.js +10 -0
- package/src/daemon/core/grounding.js +420 -0
- package/src/daemon/core/injection.js +24 -0
- package/src/daemon/core/journal.js +81 -0
- package/src/daemon/core/liber-soul.js +157 -0
- package/src/daemon/core/lock.js +32 -0
- package/src/daemon/core/memory.js +106 -0
- package/src/daemon/core/model-switch.js +58 -0
- package/src/daemon/core/policy.js +102 -0
- package/src/daemon/core/providers.js +503 -0
- package/src/daemon/core/registry.js +356 -0
- package/src/daemon/core/secrets.js +35 -0
- package/src/daemon/core/skills.js +264 -0
- package/src/daemon/core/tools/browser-tool.js +280 -0
- package/src/daemon/core/tools/file-tools.js +277 -0
- package/src/daemon/core/tools/memory-tool.js +127 -0
- package/src/daemon/core/tools/skill-tool.js +44 -0
- package/src/daemon/core/tools/task-tool.js +41 -0
- package/src/daemon/core/tools/terminal-tool.js +449 -0
- package/src/daemon/core/tools/todo-tool.js +100 -0
- package/src/daemon/core/tools/web-tool.js +181 -0
- package/src/daemon/core/trace.js +40 -0
- package/src/daemon/core/validate.js +105 -0
- package/src/daemon/core/vault.js +158 -0
- package/src/daemon/core/verifier.js +98 -0
- package/src/daemon/core/voice.js +83 -0
- package/src/daemon/core/workspace.js +199 -0
- package/src/daemon/liberd.js +1343 -0
- package/src/daemon/llm/adapters/anthropic-adapter.js +190 -0
- package/src/daemon/llm/adapters/gemini-adapter.js +148 -0
- package/src/daemon/llm/adapters/ollama-adapter.js +74 -0
- package/src/daemon/llm/adapters/openai-adapter.js +148 -0
- package/src/daemon/llm/base.js +31 -0
- package/src/daemon/llm/manager.js +134 -0
- package/src/utils/config-loader.js +109 -0
- package/src/utils/logger.js +56 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// src/daemon/llm/manager.js
|
|
2
|
+
const OllamaAdapter = require('./adapters/ollama-adapter');
|
|
3
|
+
const OpenAIAdapter = require('./adapters/openai-adapter');
|
|
4
|
+
const AnthropicAdapter = require('./adapters/anthropic-adapter');
|
|
5
|
+
const GeminiAdapter = require('./adapters/gemini-adapter');
|
|
6
|
+
|
|
7
|
+
class LLMManager {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.adapters = {
|
|
10
|
+
ollama: OllamaAdapter,
|
|
11
|
+
openai: OpenAIAdapter,
|
|
12
|
+
anthropic: AnthropicAdapter,
|
|
13
|
+
gemini: GeminiAdapter
|
|
14
|
+
// Provider baru tinggal ditambahkan di sini
|
|
15
|
+
};
|
|
16
|
+
this.activeAdapter = null;
|
|
17
|
+
this.retry = { attempts: 3, baseMs: 700 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Inisialisasi adapter berdasarkan konfigurasi di liber.yaml
|
|
22
|
+
* @param {Object} config - Bagian 'engine' dari liber.yaml
|
|
23
|
+
*/
|
|
24
|
+
initialize(config) {
|
|
25
|
+
this.engineConfig = { ...config };
|
|
26
|
+
const ProviderClass = this.adapters[config.provider];
|
|
27
|
+
if (!ProviderClass) {
|
|
28
|
+
throw new Error(`Unsupported LLM provider: ${config.provider}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(`[Liber-Daemon] Initializing LLM engine: ${config.provider} (${config.model})`);
|
|
32
|
+
this.retry = {
|
|
33
|
+
attempts: Math.max(1, Math.min(parseInt(config.retry_attempts, 10) || 3, 6)),
|
|
34
|
+
baseMs: Math.max(100, parseInt(config.retry_base_ms, 10) || 700)
|
|
35
|
+
};
|
|
36
|
+
this.activeAdapter = new ProviderClass(config);
|
|
37
|
+
return this.activeAdapter;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
isRetryable(err) {
|
|
41
|
+
const msg = String(err?.message || '');
|
|
42
|
+
if (/HTTP (400|401|403|404|422)\b/.test(msg)) return false; // client errors: retrying cannot help
|
|
43
|
+
return /HTTP (429|5\d\d)|timeout|timed out|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|fetch failed|network|socket hang up|aborted/i.test(msg);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Retry the same request on the next model of the SAME provider/endpoint.
|
|
48
|
+
* That is all a single-key setup can do: same base URL and key, different
|
|
49
|
+
* model id, so a dead or over-quota model does not kill the turn.
|
|
50
|
+
*/
|
|
51
|
+
async _withModelFallback(fn, canRetry = () => true) {
|
|
52
|
+
const base = this.engineConfig || {};
|
|
53
|
+
const chain = [base.model, ...(Array.isArray(base.fallback_models) ? base.fallback_models : [])]
|
|
54
|
+
.map(m => String(m || '').trim())
|
|
55
|
+
.filter((m, i, all) => m && all.indexOf(m) === i);
|
|
56
|
+
let index = 0;
|
|
57
|
+
let lastErr;
|
|
58
|
+
for (;;) {
|
|
59
|
+
try {
|
|
60
|
+
return await fn();
|
|
61
|
+
} catch (err) {
|
|
62
|
+
lastErr = err;
|
|
63
|
+
const next = chain[index + 1];
|
|
64
|
+
if (!next || !canRetry()) break;
|
|
65
|
+
try {
|
|
66
|
+
this.initialize({ ...base, model: next });
|
|
67
|
+
} catch (initErr) {
|
|
68
|
+
lastErr = initErr;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
this.lastFallback = { from: chain[index], to: next, error: String(err && err.message || err).slice(0, 200), at: Date.now() };
|
|
72
|
+
if (typeof this.onFallback === 'function') { try { this.onFallback(this.lastFallback); } catch { /* never break a turn */ } }
|
|
73
|
+
index++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
throw lastErr;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async generate(messages, options = {}) {
|
|
80
|
+
if (!this.activeAdapter) throw new Error("LLM Engine not initialized. Call initialize() first.");
|
|
81
|
+
return this._withModelFallback(() => this._withRetry(() => this.activeAdapter.generate(messages, options)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Native tool calling: returns { content, tool_calls, finish_reason }.
|
|
86
|
+
*/
|
|
87
|
+
async generateChat(messages, options = {}) {
|
|
88
|
+
if (!this.activeAdapter) throw new Error("LLM Engine not initialized. Call initialize() first.");
|
|
89
|
+
if (typeof this.activeAdapter.chat !== 'function') {
|
|
90
|
+
throw new Error('Active adapter does not support native tool calling.');
|
|
91
|
+
}
|
|
92
|
+
return this._withModelFallback(() => this._withRetry(() => this.activeAdapter.chat(messages, options)));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async _withRetry(fn) {
|
|
96
|
+
let lastErr;
|
|
97
|
+
for (let attempt = 1; attempt <= this.retry.attempts; attempt++) {
|
|
98
|
+
try {
|
|
99
|
+
return await fn();
|
|
100
|
+
} catch (err) {
|
|
101
|
+
lastErr = err;
|
|
102
|
+
const canRetry = attempt < this.retry.attempts && this.isRetryable(err);
|
|
103
|
+
if (!canRetry) throw err;
|
|
104
|
+
const wait = Math.min(this.retry.baseMs * 2 ** (attempt - 1), 8000);
|
|
105
|
+
console.warn(`[Liber-Daemon] LLM error (${err.message}) — retry ${attempt}/${this.retry.attempts} in ${wait}ms`);
|
|
106
|
+
await new Promise(r => setTimeout(r, wait));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
throw lastErr;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getLastUsage() {
|
|
113
|
+
return this.activeAdapter?.lastUsage || null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Chain of thought the provider returned on the most recent call, if any. */
|
|
117
|
+
getLastReasoning() {
|
|
118
|
+
return this.activeAdapter?.lastReasoning || null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async stream(messages, onChunk, options = {}) {
|
|
122
|
+
if (!this.activeAdapter) throw new Error("LLM Engine not initialized. Call initialize() first.");
|
|
123
|
+
if (typeof this.activeAdapter.stream !== 'function') {
|
|
124
|
+
throw new Error('Active adapter does not support streaming.');
|
|
125
|
+
}
|
|
126
|
+
// A stream that already emitted text cannot be replayed: falling back would
|
|
127
|
+
// duplicate the partial answer, so retry only while nothing was sent.
|
|
128
|
+
let emitted = false;
|
|
129
|
+
const guard = (t) => { emitted = true; onChunk(t); };
|
|
130
|
+
return this._withModelFallback(() => this.activeAdapter.stream(messages, guard, options), () => !emitted);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = new LLMManager();
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/utils/config-loader.js
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const yaml = require('js-yaml');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const KEY_ENV = {
|
|
7
|
+
openai: ['OPENAI_API_KEY', 'OLLAMA_CLOUD_API_KEY'],
|
|
8
|
+
anthropic: ['ANTHROPIC_API_KEY'],
|
|
9
|
+
gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
|
|
10
|
+
ollama: ['OLLAMA_CLOUD_API_KEY', 'OPENAI_API_KEY']
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// OpenAI-compatible vendors use their own key names — resolve by endpoint host.
|
|
14
|
+
const ENDPOINT_KEY_ENV = [
|
|
15
|
+
[/groq\.com/i, ['GROQ_API_KEY']],
|
|
16
|
+
[/deepseek\.com/i, ['DEEPSEEK_API_KEY']],
|
|
17
|
+
[/mistral\.ai/i, ['MISTRAL_API_KEY']],
|
|
18
|
+
[/openrouter\.ai/i, ['OPENROUTER_API_KEY']],
|
|
19
|
+
[/api\.x\.ai/i, ['XAI_API_KEY']],
|
|
20
|
+
[/together\.xyz/i, ['TOGETHER_API_KEY']],
|
|
21
|
+
[/ollama\.com/i, ['OLLAMA_CLOUD_API_KEY']],
|
|
22
|
+
[/api\.openai\.com/i, ['OPENAI_API_KEY']],
|
|
23
|
+
[/anthropic\.com/i, ['ANTHROPIC_API_KEY']],
|
|
24
|
+
[/generativelanguage\.googleapis\.com/i, ['GEMINI_API_KEY', 'GOOGLE_API_KEY']],
|
|
25
|
+
[/localhost|127\.0\.0\.1/i, []] // local Ollama needs no key
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function envVarsFromFile() {
|
|
29
|
+
try {
|
|
30
|
+
const envPath = path.resolve(process.cwd(), '.env');
|
|
31
|
+
if (!fs.existsSync(envPath)) return {};
|
|
32
|
+
return Object.fromEntries(
|
|
33
|
+
fs.readFileSync(envPath, 'utf8').split(/\r?\n/).filter(line => line.includes('=') && !line.trim().startsWith('#')).map(line => {
|
|
34
|
+
const idx = line.indexOf('=');
|
|
35
|
+
return [line.slice(0, idx).trim(), line.slice(idx + 1).trim()];
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function lookup(names) {
|
|
44
|
+
const fromEnv = names.map(n => process.env[n]).find(Boolean);
|
|
45
|
+
if (fromEnv) return fromEnv;
|
|
46
|
+
const file = envVarsFromFile();
|
|
47
|
+
return names.map(n => file[n]).find(Boolean) || null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Provider + endpoint aware key resolution: the endpoint host decides the key
|
|
52
|
+
* name first (Groq/DeepSeek/Mistral/... each have their own), then the provider.
|
|
53
|
+
* Process environment always wins over .env.
|
|
54
|
+
*/
|
|
55
|
+
function resolveApiKey(provider, endpoint) {
|
|
56
|
+
// A key stored through the UI (or imported from a CLI) wins over the
|
|
57
|
+
// environment, so the Providers page is the source of truth on this machine.
|
|
58
|
+
try {
|
|
59
|
+
const providers = require('../daemon/core/providers');
|
|
60
|
+
const stored = providers.resolveStored(provider, endpoint);
|
|
61
|
+
if (stored) return stored;
|
|
62
|
+
} catch { /* provider store unavailable: fall through to env */ }
|
|
63
|
+
|
|
64
|
+
const ep = String(endpoint || '');
|
|
65
|
+
for (const [re, names] of ENDPOINT_KEY_ENV) {
|
|
66
|
+
if (re.test(ep)) return names.length ? lookup(names) : null;
|
|
67
|
+
}
|
|
68
|
+
const envNames = KEY_ENV[String(provider || 'openai').toLowerCase()] || KEY_ENV.openai;
|
|
69
|
+
return lookup(envNames);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class ConfigLoader {
|
|
73
|
+
static load(configPath = 'liber.yaml') {
|
|
74
|
+
try {
|
|
75
|
+
// 1. Load YAML config
|
|
76
|
+
const absolutePath = path.resolve(process.cwd(), configPath);
|
|
77
|
+
const fileContents = fs.readFileSync(absolutePath, 'utf8');
|
|
78
|
+
const config = yaml.load(fileContents);
|
|
79
|
+
|
|
80
|
+
// 2. Secret Management: provider-aware key from environment or .env
|
|
81
|
+
const engine = config.agent?.engine;
|
|
82
|
+
if (engine && !engine.provider) {
|
|
83
|
+
// A null provider used to kill the whole daemon at boot
|
|
84
|
+
// ("Unsupported LLM provider: null"). Fall back to the OpenAI-compatible
|
|
85
|
+
// adapter the config comment already promises, and say so out loud.
|
|
86
|
+
engine.provider = 'openai';
|
|
87
|
+
console.warn('[config] engine.provider was empty — defaulting to the openai-compatible adapter.');
|
|
88
|
+
}
|
|
89
|
+
if (engine && !engine.endpoint) {
|
|
90
|
+
console.warn('[config] engine.endpoint is empty — every request will fail until a provider endpoint is set.');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const provider = (config.agent?.engine?.provider || 'openai').toLowerCase();
|
|
94
|
+
const envKey = resolveApiKey(provider);
|
|
95
|
+
|
|
96
|
+
if (envKey && config.agent?.engine) {
|
|
97
|
+
config.agent.engine.apiKey = envKey;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return config;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.error(`[Error] Failed to load config: ${e.message}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = ConfigLoader;
|
|
109
|
+
module.exports.resolveApiKey = resolveApiKey;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/utils/logger.js
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
class Logger {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.logFile = path.join(process.cwd(), 'liber.log');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Structured logging for production telemetry
|
|
11
|
+
info(agentId, event, details) {
|
|
12
|
+
this._write('INFO', agentId, event, details);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
warn(agentId, event, details) {
|
|
16
|
+
this._write('WARN', agentId, event, details);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
error(agentId, event, details) {
|
|
20
|
+
this._write('ERROR', agentId, event, details);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_write(level, agentId, event, details) {
|
|
24
|
+
const logEntry = {
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
level,
|
|
27
|
+
agentId,
|
|
28
|
+
event,
|
|
29
|
+
details
|
|
30
|
+
};
|
|
31
|
+
const logString = JSON.stringify(logEntry);
|
|
32
|
+
|
|
33
|
+
// Console for TUI, File for Telemetry
|
|
34
|
+
console.log(`[${level}] ${agentId} | ${event}`);
|
|
35
|
+
try {
|
|
36
|
+
this._rotateIfNeeded();
|
|
37
|
+
fs.appendFileSync(this.logFile, logString + '\n');
|
|
38
|
+
} catch {
|
|
39
|
+
// never let logging break the harness
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_rotateIfNeeded() {
|
|
44
|
+
const MAX = 5 * 1024 * 1024; // 5 MB
|
|
45
|
+
try {
|
|
46
|
+
const stat = fs.statSync(this.logFile);
|
|
47
|
+
if (stat.size > MAX) {
|
|
48
|
+
fs.renameSync(this.logFile, `${this.logFile}.1`);
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// file missing or unreadable — nothing to rotate
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = new Logger();
|