mirror of
https://github.com/Jeuners/agenttwo-tools.git
synced 2026-09-09 15:02:31 +02:00
feat: Gedächtnis mit Append-Only-Log, Traumphase und Ankerpunkten
- memory_events als append-only Log (nur INSERT), daraus Zustand deterministisch rekonstruierbar (fold/rebuild) - Traumphase konsolidiert Log in Ankerpunkte: LLM-Extraktion mit Heuristik-Fallback, Dedupe über Wort-Ähnlichkeit, Decay auf ungepinnten Ankern - einstellbares Gedächtnis-Fenster (Default 10 Schritte) statt hartkodiertem slice(-24) - wichtigste Ankerpunkte werden als System-Kontext eingespielt - neue Werkzeuge remember (schreibend, gepinnt) und recall - Gedächtnis-Panel im Frontend: Ankerliste, Pinnen, Traumphase manuell auslösen, Rekonstruktion - Eval-Skript: npm run memory:eval --workspace server
This commit is contained in:
parent
e6d55f35a3
commit
9bce768f95
22 changed files with 1388 additions and 100 deletions
74
web/dist/assets/index-BNj3u6UE.js
vendored
74
web/dist/assets/index-BNj3u6UE.js
vendored
File diff suppressed because one or more lines are too long
74
web/dist/assets/index-BvIjR1bZ.js
vendored
Normal file
74
web/dist/assets/index-BvIjR1bZ.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
7
web/dist/favicon.svg
vendored
Normal file
7
web/dist/favicon.svg
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="agenttwo-tools">
|
||||
<rect width="32" height="32" rx="7" fill="#0a0f0c"/>
|
||||
<rect x="0.75" y="0.75" width="30.5" height="30.5" rx="6.25" fill="none" stroke="#1e2c23" stroke-width="1.5"/>
|
||||
<path d="M8 10.5 L13.5 16 L8 21.5" fill="none" stroke="#3ddc84" stroke-width="3.2"
|
||||
stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="16.5" y="19" width="8.5" height="3" rx="1.5" fill="#ffb454"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 490 B |
6
web/dist/index.html
vendored
6
web/dist/index.html
vendored
|
|
@ -3,9 +3,11 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="theme-color" content="#0a0f0c" />
|
||||
<title>agenttwo-tools — qwen3 mit Vision</title>
|
||||
<script type="module" crossorigin src="/assets/index-BNj3u6UE.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BvIjEji7.css">
|
||||
<script type="module" crossorigin src="/assets/index-BvIjR1bZ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CKaNlN-w.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useVoice } from "./useVoice";
|
|||
import { Sidebar } from "./components/Sidebar";
|
||||
import { ChatMessage } from "./components/ChatMessage";
|
||||
import { Composer } from "./components/Composer";
|
||||
import { MemoryPanel } from "./components/MemoryPanel";
|
||||
import type { OpenRouterModel } from "./types";
|
||||
|
||||
const VOICE_KEY = "oxagenttwo.voiceMode";
|
||||
|
|
@ -12,6 +13,7 @@ export default function App() {
|
|||
const chat = useChat();
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [memoryOpen, setMemoryOpen] = useState(false);
|
||||
const [injectedText, setInjectedText] = useState<string | null>(null);
|
||||
const [voiceMode, setVoiceModeState] = useState(
|
||||
() => localStorage.getItem(VOICE_KEY) === "1",
|
||||
|
|
@ -209,6 +211,14 @@ export default function App() {
|
|||
>
|
||||
{voiceMode ? "🔊 Stimme: an" : "🔇 Stimme: aus"}
|
||||
</button>
|
||||
<button
|
||||
className="btn-settings"
|
||||
onClick={() => setMemoryOpen((v) => !v)}
|
||||
disabled={!chat.activeId}
|
||||
title="Gedächtnis: Ankerpunkte, Traumphase, Log-Rekonstruktion"
|
||||
>
|
||||
🧠 Gedächtnis
|
||||
</button>
|
||||
<button
|
||||
className="btn-settings"
|
||||
onClick={() => setSettingsOpen((v) => !v)}
|
||||
|
|
@ -299,6 +309,42 @@ export default function App() {
|
|||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="setting-row">
|
||||
<span>Gedächtnis-Fenster: {chat.options.memorySteps} Schritte</span>
|
||||
<input
|
||||
type="range"
|
||||
min={2}
|
||||
max={50}
|
||||
step={1}
|
||||
value={chat.options.memorySteps}
|
||||
onChange={(e) =>
|
||||
chat.setOptions({ memorySteps: Number(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="setting-row checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={chat.options.memoryAnchors}
|
||||
onChange={(e) =>
|
||||
chat.setOptions({ memoryAnchors: e.target.checked })
|
||||
}
|
||||
/>
|
||||
<span>
|
||||
Ankerpunkte ins Modell einspielen (Gedächtnis als System-Kontext)
|
||||
</span>
|
||||
</label>
|
||||
<label className="setting-row checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={chat.options.dreamAuto}
|
||||
onChange={(e) => chat.setOptions({ dreamAuto: e.target.checked })}
|
||||
/>
|
||||
<span>
|
||||
Auto-Traumphase (Konsolidierung nach 3 Min Inaktivität oder 10
|
||||
Schritten)
|
||||
</span>
|
||||
</label>
|
||||
<label className="setting-row column">
|
||||
<span>System-Prompt</span>
|
||||
<textarea
|
||||
|
|
@ -311,6 +357,13 @@ export default function App() {
|
|||
</section>
|
||||
)}
|
||||
|
||||
{memoryOpen && chat.activeId && (
|
||||
<MemoryPanel
|
||||
sessionId={chat.activeId}
|
||||
onClose={() => setMemoryOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="messages" ref={scrollRef}>
|
||||
{chat.messages.length === 0 && (
|
||||
<div className="welcome">
|
||||
|
|
|
|||
146
web/src/components/MemoryPanel.tsx
Normal file
146
web/src/components/MemoryPanel.tsx
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import type { Anchor, MemoryInfo } from "../types";
|
||||
|
||||
const KIND_LABEL: Record<string, string> = {
|
||||
fact: "Fakt",
|
||||
decision: "Entscheidung",
|
||||
preference: "Präferenz",
|
||||
entity: "Entität",
|
||||
open_question: "Offen",
|
||||
};
|
||||
|
||||
export function MemoryPanel({
|
||||
sessionId,
|
||||
onClose,
|
||||
}: {
|
||||
sessionId: string;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [info, setInfo] = useState<MemoryInfo | null>(null);
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/sessions/${sessionId}/memory`);
|
||||
const d = (await res.json()) as MemoryInfo;
|
||||
if (d.state) setInfo(d);
|
||||
} catch {
|
||||
setInfo(null);
|
||||
}
|
||||
}, [sessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
const dreamNow = async () => {
|
||||
setBusy("dream");
|
||||
try {
|
||||
await fetch(`/api/sessions/${sessionId}/dream`, { method: "POST" });
|
||||
await load();
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
};
|
||||
|
||||
const rebuild = async () => {
|
||||
setBusy("rebuild");
|
||||
try {
|
||||
await fetch(`/api/sessions/${sessionId}/memory/rebuild`, { method: "POST" });
|
||||
await load();
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
};
|
||||
|
||||
const togglePin = async (a: Anchor) => {
|
||||
await fetch(`/api/anchors/${a.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ pinned: !a.pinned }),
|
||||
});
|
||||
await load();
|
||||
};
|
||||
|
||||
const remove = async (a: Anchor) => {
|
||||
await fetch(`/api/anchors/${a.id}`, { method: "DELETE" });
|
||||
await load();
|
||||
};
|
||||
|
||||
const pending = info ? info.state.last_seq - info.state.last_dream_seq : 0;
|
||||
|
||||
return (
|
||||
<div className="memory-overlay" onClick={onClose}>
|
||||
<section className="memory-panel" onClick={(e) => e.stopPropagation()}>
|
||||
<header className="memory-header">
|
||||
<div>
|
||||
<div className="memory-title">🧠 Gedächtnis</div>
|
||||
<div className="memory-stats">
|
||||
{info
|
||||
? `${info.state.last_seq} Log-Schritte · ${info.state.dream_count}× geträumt · ${pending} unkonsolidiert`
|
||||
: "lädt …"}
|
||||
</div>
|
||||
</div>
|
||||
<button className="memory-close" onClick={onClose} title="Schließen">
|
||||
✕
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="memory-actions">
|
||||
<button onClick={() => void dreamNow()} disabled={busy !== null}>
|
||||
{busy === "dream" ? "… träumt" : "💤 Traumphase jetzt"}
|
||||
</button>
|
||||
<button onClick={() => void rebuild()} disabled={busy !== null}>
|
||||
{busy === "rebuild" ? "… faltet" : "↻ Aus Log rekonstruieren"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{info && info.anchors.length === 0 && (
|
||||
<div className="memory-empty">
|
||||
Noch keine Ankerpunkte. Sie entstehen durch die Traumphase (automatisch
|
||||
nach Inaktivität oder Manual oben) oder wenn das Modell sich etwas
|
||||
mit „remember“ merkt.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{info?.anchors.map((a) => (
|
||||
<div
|
||||
key={a.id}
|
||||
className={"anchor-row" + (a.pinned ? " pinned" : "")}
|
||||
>
|
||||
<div className="anchor-body">
|
||||
<div className="anchor-kind">
|
||||
{KIND_LABEL[a.kind] ?? a.kind} · {a.origin}
|
||||
</div>
|
||||
<div className="anchor-text">{a.text}</div>
|
||||
<div className="anchor-meta">
|
||||
Wichtigkeit {(a.importance * 100).toFixed(0)} % · {a.hits}×
|
||||
bestätigt
|
||||
</div>
|
||||
<div className="anchor-bar">
|
||||
<div
|
||||
className="anchor-bar-fill"
|
||||
style={{ width: `${Math.round(a.importance * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="anchor-actions">
|
||||
<button
|
||||
onClick={() => void togglePin(a)}
|
||||
title={a.pinned ? "Pin lösen (Decay möglich)" : "Pinnen (vor Decay geschützt)"}
|
||||
>
|
||||
{a.pinned ? "★" : "☆"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => void remove(a)}
|
||||
title="Ankerpunkt löschen"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -674,3 +674,162 @@ body {
|
|||
opacity: 0.6;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ---------- Gedächtnis ---------- */
|
||||
.memory-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(4, 8, 6, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.memory-panel {
|
||||
width: min(660px, 92vw);
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.memory-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.memory-title {
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.memory-stats {
|
||||
color: var(--text-dim);
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.memory-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-dim);
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.memory-close:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.memory-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.memory-actions button {
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
border: 1px solid var(--accent-dim);
|
||||
border-radius: 6px;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.memory-actions button:hover:not(:disabled) {
|
||||
background: var(--user-bg);
|
||||
}
|
||||
|
||||
.memory-actions button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.memory-empty {
|
||||
color: var(--text-dim);
|
||||
font-size: 13px;
|
||||
padding: 14px;
|
||||
text-align: center;
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.anchor-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
padding: 9px 12px;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.anchor-row.pinned {
|
||||
border-color: var(--accent-dim);
|
||||
}
|
||||
|
||||
.anchor-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.anchor-kind {
|
||||
font-size: 11px;
|
||||
color: var(--accent-warm);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.anchor-text {
|
||||
font-size: 13px;
|
||||
margin-top: 3px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.anchor-meta {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.anchor-bar {
|
||||
height: 3px;
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
margin-top: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.anchor-bar-fill {
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.anchor-actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.anchor-actions button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.anchor-actions button:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,44 @@ export interface ChatOptions {
|
|||
numPredict: number;
|
||||
provider: "ollama" | "openrouter";
|
||||
openrouterModel: string;
|
||||
memorySteps: number;
|
||||
memoryAnchors: boolean;
|
||||
dreamAuto: boolean;
|
||||
}
|
||||
|
||||
export interface Anchor {
|
||||
id: number;
|
||||
session_id: string;
|
||||
text: string;
|
||||
kind: string;
|
||||
importance: number;
|
||||
hits: number;
|
||||
pinned: number;
|
||||
origin: string;
|
||||
last_seq: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export interface MemoryState {
|
||||
last_seq: number;
|
||||
last_dream_seq: number;
|
||||
dream_count: number;
|
||||
}
|
||||
|
||||
export interface DreamResult {
|
||||
ok: boolean;
|
||||
events: number;
|
||||
inserted: number;
|
||||
merged: number;
|
||||
pruned: number;
|
||||
source: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface MemoryInfo {
|
||||
state: MemoryState;
|
||||
anchors: Anchor[];
|
||||
}
|
||||
|
||||
export interface OpenRouterModel {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ function loadOptions(): ChatOptions {
|
|||
numPredict: 2048,
|
||||
provider: "ollama",
|
||||
openrouterModel: "anthropic/claude-sonnet-4.5",
|
||||
memorySteps: 10,
|
||||
memoryAnchors: true,
|
||||
dreamAuto: true,
|
||||
};
|
||||
try {
|
||||
const raw = localStorage.getItem(OPTIONS_KEY);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue