0agent 1.0.5 → 1.0.7
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/dist/daemon.mjs +178 -17
- package/dist/graph.html +451 -0
- package/package.json +1 -1
package/dist/daemon.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// packages/daemon/src/ZeroAgentDaemon.ts
|
|
2
2
|
import { writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "node:fs";
|
|
3
|
-
import { resolve as
|
|
3
|
+
import { resolve as resolve3 } from "node:path";
|
|
4
4
|
import { homedir as homedir3 } from "node:os";
|
|
5
5
|
|
|
6
6
|
// packages/core/src/graph/GraphNode.ts
|
|
@@ -1691,10 +1691,12 @@ var SessionManager = class {
|
|
|
1691
1691
|
inferenceEngine;
|
|
1692
1692
|
eventBus;
|
|
1693
1693
|
graph;
|
|
1694
|
+
llm;
|
|
1694
1695
|
constructor(deps = {}) {
|
|
1695
1696
|
this.inferenceEngine = deps.inferenceEngine;
|
|
1696
1697
|
this.eventBus = deps.eventBus;
|
|
1697
1698
|
this.graph = deps.graph;
|
|
1699
|
+
this.llm = deps.llm;
|
|
1698
1700
|
}
|
|
1699
1701
|
/**
|
|
1700
1702
|
* Create a new session with status 'pending'.
|
|
@@ -1862,9 +1864,25 @@ var SessionManager = class {
|
|
|
1862
1864
|
} else {
|
|
1863
1865
|
this.addStep(session.id, "No inference engine connected \u2014 executing task directly");
|
|
1864
1866
|
}
|
|
1865
|
-
this.addStep(session.id, "
|
|
1866
|
-
|
|
1867
|
-
this.
|
|
1867
|
+
this.addStep(session.id, "Calling LLM\u2026");
|
|
1868
|
+
let output = "";
|
|
1869
|
+
if (this.llm?.isConfigured) {
|
|
1870
|
+
try {
|
|
1871
|
+
const systemPrompt = enrichedReq.context?.system_context ? String(enrichedReq.context.system_context) : `You are 0agent, a helpful AI assistant. Complete the user's task directly and concisely. If the task involves creating files, writing code, or running commands, provide the exact output needed.`;
|
|
1872
|
+
const llmRes = await this.llm.complete([
|
|
1873
|
+
{ role: "user", content: enrichedReq.task }
|
|
1874
|
+
], systemPrompt);
|
|
1875
|
+
output = llmRes.content;
|
|
1876
|
+
this.addStep(session.id, `LLM responded (${llmRes.tokens_used} tokens, ${llmRes.model})`);
|
|
1877
|
+
} catch (llmErr) {
|
|
1878
|
+
const msg = llmErr instanceof Error ? llmErr.message : String(llmErr);
|
|
1879
|
+
this.addStep(session.id, `LLM error: ${msg}`);
|
|
1880
|
+
output = `Error calling LLM: ${msg}`;
|
|
1881
|
+
}
|
|
1882
|
+
} else {
|
|
1883
|
+
output = session.plan?.reasoning ?? "No LLM configured \u2014 add API key to ~/.0agent/config.yaml";
|
|
1884
|
+
this.addStep(session.id, "No LLM configured (set api_key in ~/.0agent/config.yaml)");
|
|
1885
|
+
}
|
|
1868
1886
|
this.completeSession(session.id, {
|
|
1869
1887
|
output,
|
|
1870
1888
|
plan: session.plan ?? null,
|
|
@@ -2202,6 +2220,9 @@ var SkillRegistry = class {
|
|
|
2202
2220
|
// packages/daemon/src/HTTPServer.ts
|
|
2203
2221
|
import { Hono as Hono8 } from "hono";
|
|
2204
2222
|
import { serve } from "@hono/node-server";
|
|
2223
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
2224
|
+
import { resolve as resolve2, dirname } from "node:path";
|
|
2225
|
+
import { fileURLToPath } from "node:url";
|
|
2205
2226
|
|
|
2206
2227
|
// packages/daemon/src/routes/health.ts
|
|
2207
2228
|
import { Hono } from "hono";
|
|
@@ -2453,6 +2474,24 @@ function skillRoutes(deps) {
|
|
|
2453
2474
|
}
|
|
2454
2475
|
|
|
2455
2476
|
// packages/daemon/src/HTTPServer.ts
|
|
2477
|
+
function findGraphHtml() {
|
|
2478
|
+
const candidates = [
|
|
2479
|
+
resolve2(dirname(fileURLToPath(import.meta.url)), "graph.html"),
|
|
2480
|
+
// dev (src/)
|
|
2481
|
+
resolve2(dirname(fileURLToPath(import.meta.url)), "..", "graph.html"),
|
|
2482
|
+
// bundled (dist/../)
|
|
2483
|
+
resolve2(dirname(fileURLToPath(import.meta.url)), "..", "dist", "graph.html")
|
|
2484
|
+
];
|
|
2485
|
+
for (const p of candidates) {
|
|
2486
|
+
try {
|
|
2487
|
+
readFileSync3(p);
|
|
2488
|
+
return p;
|
|
2489
|
+
} catch {
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
return candidates[0];
|
|
2493
|
+
}
|
|
2494
|
+
var GRAPH_HTML_PATH = findGraphHtml();
|
|
2456
2495
|
var HTTPServer = class {
|
|
2457
2496
|
app;
|
|
2458
2497
|
server = null;
|
|
@@ -2467,12 +2506,19 @@ var HTTPServer = class {
|
|
|
2467
2506
|
this.app.route("/api/traces", traceRoutes({ traceStore: deps.traceStore }));
|
|
2468
2507
|
this.app.route("/api/subagents", subagentRoutes());
|
|
2469
2508
|
this.app.route("/api/skills", skillRoutes({ skillRegistry: deps.skillRegistry }));
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2509
|
+
const serveGraph = (c) => {
|
|
2510
|
+
try {
|
|
2511
|
+
const html = readFileSync3(GRAPH_HTML_PATH, "utf8");
|
|
2512
|
+
return c.html(html);
|
|
2513
|
+
} catch {
|
|
2514
|
+
return c.html("<p>Graph UI not found. Run: pnpm build</p>");
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2517
|
+
this.app.get("/", serveGraph);
|
|
2518
|
+
this.app.get("/graph", serveGraph);
|
|
2473
2519
|
}
|
|
2474
2520
|
start() {
|
|
2475
|
-
return new Promise((
|
|
2521
|
+
return new Promise((resolve5) => {
|
|
2476
2522
|
this.server = serve(
|
|
2477
2523
|
{
|
|
2478
2524
|
fetch: this.app.fetch,
|
|
@@ -2480,20 +2526,20 @@ var HTTPServer = class {
|
|
|
2480
2526
|
hostname: this.deps.host
|
|
2481
2527
|
},
|
|
2482
2528
|
() => {
|
|
2483
|
-
|
|
2529
|
+
resolve5();
|
|
2484
2530
|
}
|
|
2485
2531
|
);
|
|
2486
2532
|
});
|
|
2487
2533
|
}
|
|
2488
2534
|
stop() {
|
|
2489
|
-
return new Promise((
|
|
2535
|
+
return new Promise((resolve5, reject) => {
|
|
2490
2536
|
if (!this.server) {
|
|
2491
|
-
|
|
2537
|
+
resolve5();
|
|
2492
2538
|
return;
|
|
2493
2539
|
}
|
|
2494
2540
|
this.server.close((err) => {
|
|
2495
2541
|
if (err) reject(err);
|
|
2496
|
-
else
|
|
2542
|
+
else resolve5();
|
|
2497
2543
|
});
|
|
2498
2544
|
});
|
|
2499
2545
|
}
|
|
@@ -2502,6 +2548,107 @@ var HTTPServer = class {
|
|
|
2502
2548
|
}
|
|
2503
2549
|
};
|
|
2504
2550
|
|
|
2551
|
+
// packages/daemon/src/LLMExecutor.ts
|
|
2552
|
+
var LLMExecutor = class {
|
|
2553
|
+
constructor(config) {
|
|
2554
|
+
this.config = config;
|
|
2555
|
+
}
|
|
2556
|
+
async complete(messages, system) {
|
|
2557
|
+
switch (this.config.provider) {
|
|
2558
|
+
case "anthropic":
|
|
2559
|
+
return this.callAnthropic(messages, system);
|
|
2560
|
+
case "openai":
|
|
2561
|
+
return this.callOpenAI(messages, system);
|
|
2562
|
+
case "xai":
|
|
2563
|
+
return this.callOpenAI(messages, system, "https://api.x.ai/v1");
|
|
2564
|
+
case "gemini":
|
|
2565
|
+
return this.callOpenAI(messages, system, "https://generativelanguage.googleapis.com/v1beta/openai");
|
|
2566
|
+
case "ollama":
|
|
2567
|
+
return this.callOllama(messages, system);
|
|
2568
|
+
default:
|
|
2569
|
+
return this.callOpenAI(messages, system);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
async callAnthropic(messages, system) {
|
|
2573
|
+
const body = {
|
|
2574
|
+
model: this.config.model,
|
|
2575
|
+
max_tokens: 8192,
|
|
2576
|
+
messages: messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content }))
|
|
2577
|
+
};
|
|
2578
|
+
if (system) body.system = system;
|
|
2579
|
+
else {
|
|
2580
|
+
const sysMsg = messages.find((m) => m.role === "system");
|
|
2581
|
+
if (sysMsg) body.system = sysMsg.content;
|
|
2582
|
+
}
|
|
2583
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
2584
|
+
method: "POST",
|
|
2585
|
+
headers: {
|
|
2586
|
+
"Content-Type": "application/json",
|
|
2587
|
+
"x-api-key": this.config.api_key,
|
|
2588
|
+
"anthropic-version": "2023-06-01"
|
|
2589
|
+
},
|
|
2590
|
+
body: JSON.stringify(body)
|
|
2591
|
+
});
|
|
2592
|
+
if (!res.ok) {
|
|
2593
|
+
const err = await res.text();
|
|
2594
|
+
throw new Error(`Anthropic API error ${res.status}: ${err}`);
|
|
2595
|
+
}
|
|
2596
|
+
const data = await res.json();
|
|
2597
|
+
return {
|
|
2598
|
+
content: data.content.filter((c) => c.type === "text").map((c) => c.text).join(""),
|
|
2599
|
+
tokens_used: (data.usage?.input_tokens ?? 0) + (data.usage?.output_tokens ?? 0),
|
|
2600
|
+
model: data.model
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
async callOpenAI(messages, system, baseUrl = "https://api.openai.com/v1") {
|
|
2604
|
+
const allMessages = [];
|
|
2605
|
+
const sysContent = system ?? messages.find((m) => m.role === "system")?.content;
|
|
2606
|
+
if (sysContent) allMessages.push({ role: "system", content: sysContent });
|
|
2607
|
+
allMessages.push(...messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content })));
|
|
2608
|
+
const res = await fetch(`${this.config.base_url ?? baseUrl}/chat/completions`, {
|
|
2609
|
+
method: "POST",
|
|
2610
|
+
headers: {
|
|
2611
|
+
"Content-Type": "application/json",
|
|
2612
|
+
"Authorization": `Bearer ${this.config.api_key}`
|
|
2613
|
+
},
|
|
2614
|
+
body: JSON.stringify({
|
|
2615
|
+
model: this.config.model,
|
|
2616
|
+
messages: allMessages,
|
|
2617
|
+
max_tokens: 8192
|
|
2618
|
+
})
|
|
2619
|
+
});
|
|
2620
|
+
if (!res.ok) {
|
|
2621
|
+
const err = await res.text();
|
|
2622
|
+
throw new Error(`OpenAI API error ${res.status}: ${err}`);
|
|
2623
|
+
}
|
|
2624
|
+
const data = await res.json();
|
|
2625
|
+
return {
|
|
2626
|
+
content: data.choices[0]?.message?.content ?? "",
|
|
2627
|
+
tokens_used: data.usage?.total_tokens ?? 0,
|
|
2628
|
+
model: data.model
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
async callOllama(messages, system) {
|
|
2632
|
+
const baseUrl = this.config.base_url ?? "http://localhost:11434";
|
|
2633
|
+
const allMessages = [];
|
|
2634
|
+
const sysContent = system ?? messages.find((m) => m.role === "system")?.content;
|
|
2635
|
+
if (sysContent) allMessages.push({ role: "system", content: sysContent });
|
|
2636
|
+
allMessages.push(...messages.filter((m) => m.role !== "system"));
|
|
2637
|
+
const res = await fetch(`${baseUrl}/api/chat`, {
|
|
2638
|
+
method: "POST",
|
|
2639
|
+
headers: { "Content-Type": "application/json" },
|
|
2640
|
+
body: JSON.stringify({ model: this.config.model, messages: allMessages, stream: false })
|
|
2641
|
+
});
|
|
2642
|
+
if (!res.ok) throw new Error(`Ollama error ${res.status}`);
|
|
2643
|
+
const data = await res.json();
|
|
2644
|
+
return { content: data.message.content, tokens_used: data.eval_count ?? 0, model: this.config.model };
|
|
2645
|
+
}
|
|
2646
|
+
get isConfigured() {
|
|
2647
|
+
if (this.config.provider === "ollama") return true;
|
|
2648
|
+
return !!this.config.api_key?.trim();
|
|
2649
|
+
}
|
|
2650
|
+
};
|
|
2651
|
+
|
|
2505
2652
|
// packages/daemon/src/ZeroAgentDaemon.ts
|
|
2506
2653
|
var ZeroAgentDaemon = class {
|
|
2507
2654
|
config = null;
|
|
@@ -2517,11 +2664,11 @@ var ZeroAgentDaemon = class {
|
|
|
2517
2664
|
startedAt = 0;
|
|
2518
2665
|
pidFilePath;
|
|
2519
2666
|
constructor() {
|
|
2520
|
-
this.pidFilePath =
|
|
2667
|
+
this.pidFilePath = resolve3(homedir3(), ".0agent", "daemon.pid");
|
|
2521
2668
|
}
|
|
2522
2669
|
async start(opts) {
|
|
2523
2670
|
this.config = await loadConfig(opts?.config_path);
|
|
2524
|
-
const dotDir =
|
|
2671
|
+
const dotDir = resolve3(homedir3(), ".0agent");
|
|
2525
2672
|
if (!existsSync3(dotDir)) {
|
|
2526
2673
|
mkdirSync2(dotDir, { recursive: true });
|
|
2527
2674
|
}
|
|
@@ -2534,10 +2681,24 @@ var ZeroAgentDaemon = class {
|
|
|
2534
2681
|
this.inferenceEngine = new InferenceEngine(this.graph, resolver, policy);
|
|
2535
2682
|
this.skillRegistry = new SkillRegistry();
|
|
2536
2683
|
await this.skillRegistry.loadAll();
|
|
2684
|
+
const defaultLLM = this.config.llm_providers.find((p) => p.is_default) ?? this.config.llm_providers[0];
|
|
2685
|
+
const llmExecutor = defaultLLM ? new LLMExecutor({
|
|
2686
|
+
provider: defaultLLM.provider,
|
|
2687
|
+
model: defaultLLM.model,
|
|
2688
|
+
api_key: defaultLLM.api_key ?? "",
|
|
2689
|
+
base_url: defaultLLM.base_url
|
|
2690
|
+
}) : void 0;
|
|
2691
|
+
if (llmExecutor?.isConfigured) {
|
|
2692
|
+
console.log(`[0agent] LLM: ${defaultLLM?.provider}/${defaultLLM?.model}`);
|
|
2693
|
+
} else {
|
|
2694
|
+
console.warn("[0agent] No LLM API key configured \u2014 tasks will not call the LLM");
|
|
2695
|
+
}
|
|
2537
2696
|
this.eventBus = new WebSocketEventBus();
|
|
2538
2697
|
this.sessionManager = new SessionManager({
|
|
2539
2698
|
inferenceEngine: this.inferenceEngine,
|
|
2540
|
-
eventBus: this.eventBus
|
|
2699
|
+
eventBus: this.eventBus,
|
|
2700
|
+
graph: this.graph,
|
|
2701
|
+
llm: llmExecutor
|
|
2541
2702
|
});
|
|
2542
2703
|
this.backgroundWorkers = new BackgroundWorkers({
|
|
2543
2704
|
graph: this.graph,
|
|
@@ -2623,10 +2784,10 @@ var ZeroAgentDaemon = class {
|
|
|
2623
2784
|
};
|
|
2624
2785
|
|
|
2625
2786
|
// packages/daemon/src/start.ts
|
|
2626
|
-
import { resolve as
|
|
2787
|
+
import { resolve as resolve4 } from "node:path";
|
|
2627
2788
|
import { homedir as homedir4 } from "node:os";
|
|
2628
2789
|
import { existsSync as existsSync4 } from "node:fs";
|
|
2629
|
-
var CONFIG_PATH = process.env["ZEROAGENT_CONFIG"] ??
|
|
2790
|
+
var CONFIG_PATH = process.env["ZEROAGENT_CONFIG"] ?? resolve4(homedir4(), ".0agent", "config.yaml");
|
|
2630
2791
|
if (!existsSync4(CONFIG_PATH)) {
|
|
2631
2792
|
console.error(`
|
|
2632
2793
|
0agent is not initialised.
|
package/dist/graph.html
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>0agent — Knowledge Graph</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body { background: #0d0d0d; color: #e0e0e0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; overflow: hidden; }
|
|
10
|
+
|
|
11
|
+
#graph { width: 100vw; height: 100vh; }
|
|
12
|
+
|
|
13
|
+
/* Top bar */
|
|
14
|
+
#topbar {
|
|
15
|
+
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
|
16
|
+
display: flex; align-items: center; gap: 12px;
|
|
17
|
+
padding: 10px 16px; background: rgba(13,13,13,0.85);
|
|
18
|
+
border-bottom: 1px solid #222; backdrop-filter: blur(8px);
|
|
19
|
+
}
|
|
20
|
+
#topbar h1 { font-size: 15px; font-weight: 600; color: #7aa2f7; letter-spacing: -0.3px; }
|
|
21
|
+
#topbar .stats { font-size: 12px; color: #555; }
|
|
22
|
+
#topbar .ws-dot {
|
|
23
|
+
width: 7px; height: 7px; border-radius: 50%; background: #555;
|
|
24
|
+
transition: background 0.3s;
|
|
25
|
+
}
|
|
26
|
+
#topbar .ws-dot.live { background: #9ece6a; box-shadow: 0 0 6px #9ece6a88; }
|
|
27
|
+
#topbar .spacer { flex: 1; }
|
|
28
|
+
#topbar input {
|
|
29
|
+
background: #1a1a1a; border: 1px solid #333; color: #e0e0e0;
|
|
30
|
+
padding: 5px 10px; border-radius: 6px; font-size: 12px; width: 200px;
|
|
31
|
+
outline: none;
|
|
32
|
+
}
|
|
33
|
+
#topbar input:focus { border-color: #7aa2f7; }
|
|
34
|
+
#topbar select {
|
|
35
|
+
background: #1a1a1a; border: 1px solid #333; color: #e0e0e0;
|
|
36
|
+
padding: 5px 8px; border-radius: 6px; font-size: 12px; outline: none;
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Side panel */
|
|
41
|
+
#panel {
|
|
42
|
+
position: fixed; top: 0; right: 0; bottom: 0; width: 320px; z-index: 90;
|
|
43
|
+
background: rgba(13,13,13,0.95); border-left: 1px solid #222;
|
|
44
|
+
transform: translateX(100%); transition: transform 0.25s ease;
|
|
45
|
+
overflow-y: auto; padding: 56px 0 16px;
|
|
46
|
+
backdrop-filter: blur(12px);
|
|
47
|
+
}
|
|
48
|
+
#panel.open { transform: translateX(0); }
|
|
49
|
+
#panel-close {
|
|
50
|
+
position: absolute; top: 44px; right: 12px;
|
|
51
|
+
background: none; border: none; color: #555; cursor: pointer;
|
|
52
|
+
font-size: 18px; line-height: 1;
|
|
53
|
+
}
|
|
54
|
+
#panel-close:hover { color: #e0e0e0; }
|
|
55
|
+
.panel-section { padding: 12px 16px; border-bottom: 1px solid #1a1a1a; }
|
|
56
|
+
.panel-section:last-child { border-bottom: none; }
|
|
57
|
+
.panel-label { font-size: 10px; font-weight: 600; text-transform: uppercase;
|
|
58
|
+
letter-spacing: 0.8px; color: #555; margin-bottom: 6px; }
|
|
59
|
+
.panel-value { font-size: 13px; color: #e0e0e0; word-break: break-word; }
|
|
60
|
+
.panel-badge {
|
|
61
|
+
display: inline-block; font-size: 10px; font-weight: 600; padding: 2px 8px;
|
|
62
|
+
border-radius: 10px; margin-right: 4px; margin-bottom: 4px;
|
|
63
|
+
}
|
|
64
|
+
.edge-row {
|
|
65
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
66
|
+
padding: 4px 0; border-bottom: 1px solid #111; font-size: 12px;
|
|
67
|
+
}
|
|
68
|
+
.edge-row:last-child { border-bottom: none; }
|
|
69
|
+
.edge-label { color: #8080a0; flex: 1; }
|
|
70
|
+
.edge-target { color: #7aa2f7; cursor: pointer; }
|
|
71
|
+
.edge-target:hover { text-decoration: underline; }
|
|
72
|
+
.weight-bar {
|
|
73
|
+
width: 40px; height: 4px; border-radius: 2px; background: #222;
|
|
74
|
+
position: relative; overflow: hidden;
|
|
75
|
+
}
|
|
76
|
+
.weight-fill { height: 100%; border-radius: 2px; transition: width 0.5s; }
|
|
77
|
+
.personality-line { font-size: 12px; color: #aaa; margin-bottom: 4px; line-height: 1.5; }
|
|
78
|
+
|
|
79
|
+
/* Tooltip */
|
|
80
|
+
#tooltip {
|
|
81
|
+
position: fixed; z-index: 200; pointer-events: none;
|
|
82
|
+
background: rgba(13,13,13,0.95); border: 1px solid #333;
|
|
83
|
+
border-radius: 8px; padding: 8px 12px; font-size: 12px;
|
|
84
|
+
max-width: 220px; display: none;
|
|
85
|
+
}
|
|
86
|
+
#tooltip .tip-label { font-weight: 600; color: #e0e0e0; margin-bottom: 3px; }
|
|
87
|
+
#tooltip .tip-type { font-size: 10px; color: #555; text-transform: uppercase; }
|
|
88
|
+
#tooltip .tip-weight { font-size: 11px; color: #9ece6a; margin-top: 3px; }
|
|
89
|
+
|
|
90
|
+
/* Legend */
|
|
91
|
+
#legend {
|
|
92
|
+
position: fixed; bottom: 16px; left: 16px; z-index: 100;
|
|
93
|
+
background: rgba(13,13,13,0.85); border: 1px solid #222;
|
|
94
|
+
border-radius: 8px; padding: 10px 14px; font-size: 11px;
|
|
95
|
+
backdrop-filter: blur(8px);
|
|
96
|
+
}
|
|
97
|
+
#legend .legend-title { font-size: 10px; font-weight: 600; text-transform: uppercase;
|
|
98
|
+
letter-spacing: 0.6px; color: #555; margin-bottom: 6px; }
|
|
99
|
+
.legend-item { display: flex; align-items: center; gap: 7px; margin-bottom: 4px; color: #aaa; }
|
|
100
|
+
.legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
101
|
+
|
|
102
|
+
/* Weight change flash */
|
|
103
|
+
@keyframes flash { 0%,100%{opacity:1} 50%{opacity:0.3} }
|
|
104
|
+
</style>
|
|
105
|
+
</head>
|
|
106
|
+
<body>
|
|
107
|
+
|
|
108
|
+
<div id="topbar">
|
|
109
|
+
<span class="ws-dot" id="wsdot"></span>
|
|
110
|
+
<h1>0agent</h1>
|
|
111
|
+
<span class="stats" id="stats">loading…</span>
|
|
112
|
+
<span class="spacer"></span>
|
|
113
|
+
<input type="text" id="search" placeholder="Search nodes…" />
|
|
114
|
+
<select id="filter">
|
|
115
|
+
<option value="all">All types</option>
|
|
116
|
+
<option value="entity">Entity</option>
|
|
117
|
+
<option value="context">Context</option>
|
|
118
|
+
<option value="strategy">Strategy</option>
|
|
119
|
+
<option value="plan">Plan</option>
|
|
120
|
+
<option value="step">Step</option>
|
|
121
|
+
<option value="signal">Signal</option>
|
|
122
|
+
<option value="outcome">Outcome</option>
|
|
123
|
+
<option value="hypothesis">Hypothesis</option>
|
|
124
|
+
</select>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div id="graph"></div>
|
|
128
|
+
|
|
129
|
+
<div id="panel">
|
|
130
|
+
<button id="panel-close">✕</button>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div id="tooltip"></div>
|
|
134
|
+
|
|
135
|
+
<div id="legend">
|
|
136
|
+
<div class="legend-title">Node types</div>
|
|
137
|
+
<div class="legend-item"><span class="legend-dot" style="background:#7aa2f7"></span>Entity</div>
|
|
138
|
+
<div class="legend-item"><span class="legend-dot" style="background:#9ece6a"></span>Context</div>
|
|
139
|
+
<div class="legend-item"><span class="legend-dot" style="background:#e0af68"></span>Strategy</div>
|
|
140
|
+
<div class="legend-item"><span class="legend-dot" style="background:#bb9af7"></span>Plan</div>
|
|
141
|
+
<div class="legend-item"><span class="legend-dot" style="background:#f7768e"></span>Step</div>
|
|
142
|
+
<div class="legend-item"><span class="legend-dot" style="background:#2ac3de"></span>Signal</div>
|
|
143
|
+
<div class="legend-item"><span class="legend-dot" style="background:#1abc9c"></span>Outcome</div>
|
|
144
|
+
<div class="legend-item"><span class="legend-dot" style="background:#ff9e64"></span>Hypothesis</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<!-- 3D Force Graph (WebGL) — same engine Obsidian uses -->
|
|
148
|
+
<script src="https://unpkg.com/3d-force-graph@1.73.2/dist/3d-force-graph.min.js"></script>
|
|
149
|
+
|
|
150
|
+
<script>
|
|
151
|
+
const NODE_COLORS = {
|
|
152
|
+
entity: '#7aa2f7',
|
|
153
|
+
context: '#9ece6a',
|
|
154
|
+
strategy: '#e0af68',
|
|
155
|
+
plan: '#bb9af7',
|
|
156
|
+
step: '#f7768e',
|
|
157
|
+
signal: '#2ac3de',
|
|
158
|
+
outcome: '#1abc9c',
|
|
159
|
+
tool: '#73daca',
|
|
160
|
+
constraint: '#565f89',
|
|
161
|
+
hypothesis: '#ff9e64',
|
|
162
|
+
};
|
|
163
|
+
const DEFAULT_COLOR = '#565f89';
|
|
164
|
+
|
|
165
|
+
// Graph state
|
|
166
|
+
let graphData = { nodes: [], links: [] };
|
|
167
|
+
let nodeMap = new Map(); // id → node
|
|
168
|
+
let edgeMap = new Map(); // id → edge
|
|
169
|
+
let selectedNode = null;
|
|
170
|
+
let filterType = 'all';
|
|
171
|
+
let searchQuery = '';
|
|
172
|
+
|
|
173
|
+
// ─── Init 3D graph ─────────────────────────────────────────────────────────
|
|
174
|
+
const Graph = ForceGraph3D()(document.getElementById('graph'))
|
|
175
|
+
.backgroundColor('#0d0d0d')
|
|
176
|
+
.nodeId('id')
|
|
177
|
+
.nodeLabel(n => `${n.label} (${n.type})`)
|
|
178
|
+
.nodeColor(n => {
|
|
179
|
+
const c = NODE_COLORS[n.type] || DEFAULT_COLOR;
|
|
180
|
+
if (searchQuery && !n.label.toLowerCase().includes(searchQuery)) return '#222';
|
|
181
|
+
if (filterType !== 'all' && n.type !== filterType) return '#1a1a1a';
|
|
182
|
+
if (n._flash) return '#ffffff';
|
|
183
|
+
return c;
|
|
184
|
+
})
|
|
185
|
+
.nodeVal(n => Math.max(1, Math.log2((n.visit_count || 1) + 1) * 3))
|
|
186
|
+
.nodeOpacity(0.9)
|
|
187
|
+
.linkColor(e => {
|
|
188
|
+
const w = e.weight ?? 0.5;
|
|
189
|
+
if (w >= 0.7) return '#9ece6a88';
|
|
190
|
+
if (w >= 0.4) return '#e0af6888';
|
|
191
|
+
return '#f7768e88';
|
|
192
|
+
})
|
|
193
|
+
.linkWidth(e => Math.max(0.3, (e.weight ?? 0.5) * 3))
|
|
194
|
+
.linkDirectionalArrowLength(4)
|
|
195
|
+
.linkDirectionalArrowRelPos(1)
|
|
196
|
+
.linkDirectionalParticles(e => e.weight > 0.7 ? 2 : 0)
|
|
197
|
+
.linkDirectionalParticleSpeed(0.006)
|
|
198
|
+
.linkDirectionalParticleColor(e => NODE_COLORS[e._toType] || '#7aa2f788')
|
|
199
|
+
.onNodeClick(node => showPanel(node))
|
|
200
|
+
.onNodeHover(node => showTooltip(node))
|
|
201
|
+
.onBackgroundClick(() => closePanel());
|
|
202
|
+
|
|
203
|
+
// Responsive resize
|
|
204
|
+
window.addEventListener('resize', () => {
|
|
205
|
+
Graph.width(window.innerWidth).height(window.innerHeight);
|
|
206
|
+
});
|
|
207
|
+
Graph.width(window.innerWidth).height(window.innerHeight);
|
|
208
|
+
|
|
209
|
+
// ─── Data loading ───────────────────────────────────────────────────────────
|
|
210
|
+
async function loadGraph() {
|
|
211
|
+
try {
|
|
212
|
+
const [nodesRes, edgesRes] = await Promise.all([
|
|
213
|
+
fetch('/api/graph/nodes?limit=500'),
|
|
214
|
+
fetch('/api/graph/edges?limit=2000'),
|
|
215
|
+
]);
|
|
216
|
+
const nodesData = await nodesRes.json();
|
|
217
|
+
const edgesData = await edgesRes.json();
|
|
218
|
+
|
|
219
|
+
const nodes = (Array.isArray(nodesData) ? nodesData : nodesData.nodes ?? []);
|
|
220
|
+
const edges = (Array.isArray(edgesData) ? edgesData : edgesData.edges ?? []);
|
|
221
|
+
|
|
222
|
+
nodeMap.clear();
|
|
223
|
+
edgeMap.clear();
|
|
224
|
+
|
|
225
|
+
for (const n of nodes) nodeMap.set(n.id, n);
|
|
226
|
+
|
|
227
|
+
const links = edges
|
|
228
|
+
.filter(e => nodeMap.has(e.from_node) && nodeMap.has(e.to_node))
|
|
229
|
+
.map(e => ({
|
|
230
|
+
...e,
|
|
231
|
+
source: e.from_node,
|
|
232
|
+
target: e.to_node,
|
|
233
|
+
_toType: nodeMap.get(e.to_node)?.type,
|
|
234
|
+
}));
|
|
235
|
+
|
|
236
|
+
for (const e of links) edgeMap.set(e.id, e);
|
|
237
|
+
|
|
238
|
+
graphData = { nodes, links };
|
|
239
|
+
Graph.graphData(graphData);
|
|
240
|
+
updateStats();
|
|
241
|
+
} catch (e) {
|
|
242
|
+
console.error('Failed to load graph:', e);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function updateStats() {
|
|
247
|
+
document.getElementById('stats').textContent =
|
|
248
|
+
`${graphData.nodes.length} nodes · ${graphData.links.length} edges`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ─── WebSocket live updates ─────────────────────────────────────────────────
|
|
252
|
+
const wsDot = document.getElementById('wsdot');
|
|
253
|
+
let ws;
|
|
254
|
+
|
|
255
|
+
function connectWS() {
|
|
256
|
+
ws = new WebSocket(`ws://${location.host}/ws`);
|
|
257
|
+
ws.onopen = () => {
|
|
258
|
+
wsDot.classList.add('live');
|
|
259
|
+
ws.send(JSON.stringify({ type: 'subscribe', topics: ['graph', 'sessions', 'stats'] }));
|
|
260
|
+
};
|
|
261
|
+
ws.onclose = () => {
|
|
262
|
+
wsDot.classList.remove('live');
|
|
263
|
+
setTimeout(connectWS, 3000);
|
|
264
|
+
};
|
|
265
|
+
ws.onmessage = (e) => {
|
|
266
|
+
const event = JSON.parse(e.data);
|
|
267
|
+
handleEvent(event);
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function handleEvent(event) {
|
|
272
|
+
switch (event.type) {
|
|
273
|
+
case 'graph.weight_updated': {
|
|
274
|
+
const link = edgeMap.get(event.edge_id);
|
|
275
|
+
if (link) {
|
|
276
|
+
link.weight = event.new_weight;
|
|
277
|
+
// Flash the connected nodes
|
|
278
|
+
const srcNode = nodeMap.get(link.from_node || link.source?.id);
|
|
279
|
+
if (srcNode) { srcNode._flash = true; setTimeout(() => { srcNode._flash = false; Graph.refresh(); }, 600); }
|
|
280
|
+
Graph.refresh();
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
case 'daemon.stats':
|
|
285
|
+
// Reload graph periodically when stats show growth
|
|
286
|
+
if (event.graph_nodes > graphData.nodes.length) loadGraph();
|
|
287
|
+
break;
|
|
288
|
+
case 'session.completed':
|
|
289
|
+
// Small delay then reload to pick up new nodes
|
|
290
|
+
setTimeout(loadGraph, 800);
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ─── Side panel ─────────────────────────────────────────────────────────────
|
|
296
|
+
function showPanel(node) {
|
|
297
|
+
if (!node) return;
|
|
298
|
+
selectedNode = node;
|
|
299
|
+
const panel = document.getElementById('panel');
|
|
300
|
+
panel.classList.add('open');
|
|
301
|
+
|
|
302
|
+
// Build content
|
|
303
|
+
const edges = graphData.links.filter(
|
|
304
|
+
l => (l.source?.id ?? l.source) === node.id || (l.target?.id ?? l.target) === node.id
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const color = NODE_COLORS[node.type] || DEFAULT_COLOR;
|
|
308
|
+
|
|
309
|
+
let html = `
|
|
310
|
+
<div class="panel-section">
|
|
311
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
|
|
312
|
+
<span style="width:10px;height:10px;border-radius:50%;background:${color};flex-shrink:0"></span>
|
|
313
|
+
<span style="font-size:15px;font-weight:600">${escHtml(node.label)}</span>
|
|
314
|
+
</div>
|
|
315
|
+
<span class="panel-badge" style="background:${color}22;color:${color}">${node.type}</span>
|
|
316
|
+
${node.metadata?.is_skill ? '<span class="panel-badge" style="background:#7aa2f722;color:#7aa2f7">skill</span>' : ''}
|
|
317
|
+
</div>
|
|
318
|
+
<div class="panel-section">
|
|
319
|
+
<div class="panel-label">Stats</div>
|
|
320
|
+
<div class="panel-value">Visits: ${node.visit_count ?? 0}</div>
|
|
321
|
+
<div class="panel-value" style="color:#555;font-size:11px;margin-top:3px">
|
|
322
|
+
Last seen: ${node.last_seen ? new Date(node.last_seen).toLocaleString() : '—'}
|
|
323
|
+
</div>
|
|
324
|
+
</div>`;
|
|
325
|
+
|
|
326
|
+
// Personality profile (for entity nodes)
|
|
327
|
+
if (node.type === 'entity') {
|
|
328
|
+
const personalityEdge = graphData.links.find(
|
|
329
|
+
l => (l.source?.id ?? l.source) === node.id && nodeMap.get(l.target?.id ?? l.target)?.label === '__personality_profile__'
|
|
330
|
+
);
|
|
331
|
+
if (personalityEdge) {
|
|
332
|
+
const profileNode = nodeMap.get(personalityEdge.target?.id ?? personalityEdge.target);
|
|
333
|
+
if (profileNode?.content?.[0]?.data) {
|
|
334
|
+
try {
|
|
335
|
+
const p = JSON.parse(profileNode.content[0].data);
|
|
336
|
+
html += `
|
|
337
|
+
<div class="panel-section">
|
|
338
|
+
<div class="panel-label">Personality (${p.interaction_count} interactions)</div>
|
|
339
|
+
${p.communication_style ? `<div class="personality-line"><b>Style:</b> ${escHtml(p.communication_style)}</div>` : ''}
|
|
340
|
+
${p.working_context ? `<div class="personality-line"><b>Context:</b> ${escHtml(p.working_context)}</div>` : ''}
|
|
341
|
+
${p.timezone !== 'UTC' && p.timezone ? `<div class="personality-line"><b>TZ:</b> ${escHtml(p.timezone)}</div>` : ''}
|
|
342
|
+
${p.response_preferences?.length ? `<div class="personality-line"><b>Prefs:</b> ${p.response_preferences.map(escHtml).join(', ')}</div>` : ''}
|
|
343
|
+
</div>`;
|
|
344
|
+
} catch {}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Edges
|
|
350
|
+
if (edges.length > 0) {
|
|
351
|
+
html += `<div class="panel-section"><div class="panel-label">Connections (${edges.length})</div>`;
|
|
352
|
+
for (const edge of edges.slice(0, 20)) {
|
|
353
|
+
const srcId = edge.source?.id ?? edge.source;
|
|
354
|
+
const tgtId = edge.target?.id ?? edge.target;
|
|
355
|
+
const isOut = srcId === node.id;
|
|
356
|
+
const otherId = isOut ? tgtId : srcId;
|
|
357
|
+
const other = nodeMap.get(otherId);
|
|
358
|
+
const w = edge.weight ?? 0.5;
|
|
359
|
+
const wColor = w >= 0.7 ? '#9ece6a' : w >= 0.4 ? '#e0af68' : '#f7768e';
|
|
360
|
+
html += `
|
|
361
|
+
<div class="edge-row">
|
|
362
|
+
<span class="edge-label">${isOut ? '→' : '←'} ${escHtml(edge.type)}</span>
|
|
363
|
+
<span class="edge-target" onclick="focusNode('${otherId}')">${escHtml(other?.label ?? otherId)}</span>
|
|
364
|
+
<div style="display:flex;align-items:center;gap:4px;margin-left:8px">
|
|
365
|
+
<div class="weight-bar"><div class="weight-fill" style="width:${(w*100).toFixed(0)}%;background:${wColor}"></div></div>
|
|
366
|
+
<span style="font-size:10px;color:${wColor};min-width:28px">${w.toFixed(2)}</span>
|
|
367
|
+
</div>
|
|
368
|
+
</div>`;
|
|
369
|
+
}
|
|
370
|
+
if (edges.length > 20) html += `<div style="font-size:11px;color:#555;margin-top:6px">+${edges.length - 20} more</div>`;
|
|
371
|
+
html += '</div>';
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Node metadata
|
|
375
|
+
if (node.metadata && Object.keys(node.metadata).length > 0) {
|
|
376
|
+
const meta = Object.entries(node.metadata)
|
|
377
|
+
.filter(([k]) => !['is_personality_profile'].includes(k))
|
|
378
|
+
.slice(0, 8);
|
|
379
|
+
if (meta.length > 0) {
|
|
380
|
+
html += `<div class="panel-section"><div class="panel-label">Metadata</div>`;
|
|
381
|
+
for (const [k, v] of meta) {
|
|
382
|
+
html += `<div style="font-size:11px;color:#555;margin-bottom:2px"><span style="color:#7aa2f7">${escHtml(k)}</span>: ${escHtml(String(v))}</div>`;
|
|
383
|
+
}
|
|
384
|
+
html += '</div>';
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
panel.innerHTML = `<button id="panel-close" onclick="closePanel()">✕</button>${html}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function closePanel() {
|
|
392
|
+
document.getElementById('panel').classList.remove('open');
|
|
393
|
+
selectedNode = null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function focusNode(id) {
|
|
397
|
+
const node = nodeMap.get(id);
|
|
398
|
+
if (!node) return;
|
|
399
|
+
Graph.centerAt(node.x, node.y, node.z, 600);
|
|
400
|
+
Graph.zoom(2, 600);
|
|
401
|
+
setTimeout(() => showPanel(node), 650);
|
|
402
|
+
}
|
|
403
|
+
window.focusNode = focusNode;
|
|
404
|
+
|
|
405
|
+
// ─── Tooltip ────────────────────────────────────────────────────────────────
|
|
406
|
+
const tooltip = document.getElementById('tooltip');
|
|
407
|
+
|
|
408
|
+
function showTooltip(node) {
|
|
409
|
+
if (!node) { tooltip.style.display = 'none'; return; }
|
|
410
|
+
const edges = graphData.links.filter(
|
|
411
|
+
l => (l.source?.id ?? l.source) === node.id || (l.target?.id ?? l.target) === node.id
|
|
412
|
+
);
|
|
413
|
+
const avgWeight = edges.length
|
|
414
|
+
? (edges.reduce((s, e) => s + (e.weight ?? 0.5), 0) / edges.length).toFixed(2)
|
|
415
|
+
: '—';
|
|
416
|
+
tooltip.innerHTML = `
|
|
417
|
+
<div class="tip-label">${escHtml(node.label)}</div>
|
|
418
|
+
<div class="tip-type">${node.type}</div>
|
|
419
|
+
<div class="tip-weight">${edges.length} connections · avg weight ${avgWeight}</div>`;
|
|
420
|
+
tooltip.style.display = 'block';
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
document.addEventListener('mousemove', e => {
|
|
424
|
+
tooltip.style.left = (e.clientX + 14) + 'px';
|
|
425
|
+
tooltip.style.top = (e.clientY - 10) + 'px';
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// ─── Search & filter ─────────────────────────────────────────────────────────
|
|
429
|
+
document.getElementById('search').addEventListener('input', e => {
|
|
430
|
+
searchQuery = e.target.value.toLowerCase();
|
|
431
|
+
Graph.refresh();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
document.getElementById('filter').addEventListener('change', e => {
|
|
435
|
+
filterType = e.target.value;
|
|
436
|
+
Graph.refresh();
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
440
|
+
function escHtml(s) {
|
|
441
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ─── Boot ─────────────────────────────────────────────────────────────────────
|
|
445
|
+
loadGraph();
|
|
446
|
+
connectWS();
|
|
447
|
+
// Reload every 30s to catch any missed updates
|
|
448
|
+
setInterval(loadGraph, 30_000);
|
|
449
|
+
</script>
|
|
450
|
+
</body>
|
|
451
|
+
</html>
|