Agent/subagent support
This commit is contained in:
147
src/memory.ts
147
src/memory.ts
@@ -1,6 +1,143 @@
|
||||
import {LLMRequest, LLMMessage} from './llm.ts';
|
||||
import {MemoryCache} from './memory-cache.ts';
|
||||
import {AiTool} from './tools.ts';
|
||||
import {KDPoint, KDTree} from './kd-tree.ts';
|
||||
|
||||
export function buildMemoryGraph(memories: Memory[] | MemoryCache): MemoryNode[] {
|
||||
const mems = memories instanceof MemoryCache ? memories.memories : memories;
|
||||
const nameSet = new Set(mems.map(m => m.name));
|
||||
const ghosts = new Set<string>();
|
||||
|
||||
const nodes: MemoryNode[] = mems.map(m => {
|
||||
const {links, backlinks} = extractMetadata(m.content);
|
||||
return {
|
||||
name: m.name,
|
||||
missing: false,
|
||||
links,
|
||||
backlinks,
|
||||
};
|
||||
});
|
||||
|
||||
for (const node of nodes) {
|
||||
for (const link of node.links) {
|
||||
if (!nameSet.has(link)) ghosts.add(link);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
...nodes,
|
||||
...[...ghosts].map(name => ({
|
||||
name,
|
||||
missing: true,
|
||||
links: [],
|
||||
backlinks: nodes
|
||||
.filter(n => n.links.includes(name))
|
||||
.map(n => n.name),
|
||||
}))
|
||||
];
|
||||
}
|
||||
|
||||
export function renderMemoryGraph(nodes) {
|
||||
if (!nodes.length) return 'No memories yet.';
|
||||
|
||||
const groups = new Map();
|
||||
for (const node of nodes) {
|
||||
const [prefix, ...rest] = node.name.split('/');
|
||||
const group = rest.length ? prefix : 'Root';
|
||||
const label = rest.length ? rest.join('/') : node.name;
|
||||
if (!groups.has(group)) groups.set(group, []);
|
||||
groups.get(group).push({...node, label});
|
||||
}
|
||||
|
||||
const ghostCount = nodes.filter(n => n.missing).length;
|
||||
const lines = [`Memory Graph (${nodes.length} nodes, ${ghostCount} ghost${ghostCount === 1 ? '' : 's'})`, ''];
|
||||
|
||||
for (const group of [...groups.keys()].sort()) {
|
||||
const items = groups.get(group).sort((a, b) => a.label.localeCompare(b.label));
|
||||
lines.push(`${group}/`);
|
||||
items.forEach((n, i) => {
|
||||
const last = i === items.length - 1;
|
||||
const branch = last ? '└─' : '├─';
|
||||
const pad = last ? ' ' : '│ ';
|
||||
const tag = n.missing ? ' (ghost)' : '';
|
||||
lines.push(` ${branch} ${n.label}${tag}`);
|
||||
if (n.links.length) lines.push(` ${pad} → ${n.links.join(', ')}`);
|
||||
if (n.backlinks.length) lines.push(` ${pad} ← ${n.backlinks.join(', ')}`);
|
||||
});
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n').trimEnd();
|
||||
}
|
||||
|
||||
export class MemoryCache {
|
||||
private tree: KDTree<MemoryRef>;
|
||||
public memories: Memory[];
|
||||
|
||||
get length() { return this.memories.length; }
|
||||
|
||||
constructor(memories: Memory[]) {
|
||||
this.memories = memories;
|
||||
this.tree = this.buildTree();
|
||||
}
|
||||
|
||||
private buildTree(): KDTree<MemoryRef> {
|
||||
const embedded = this.memories.filter(m => m.embedding?.length);
|
||||
if (!embedded.length) return new KDTree<MemoryRef>(0);
|
||||
|
||||
const dims = embedded[0].embedding.length;
|
||||
const points: KDPoint<MemoryRef>[] = embedded.map(m => ({
|
||||
vector: m.embedding,
|
||||
payload: {name: m.name, description: m.description},
|
||||
}));
|
||||
|
||||
return new KDTree<MemoryRef>(dims, 'cosine', points);
|
||||
}
|
||||
|
||||
search(query: number[], limit: number): MemoryRef[] {
|
||||
const results = this.tree.knn(query, limit);
|
||||
return results.map(r => r.point.payload);
|
||||
}
|
||||
|
||||
add(memory: Memory): void {
|
||||
this.memories.push(memory);
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
update(memory: Memory): void {
|
||||
const idx = this.memories.findIndex(m => m.name === memory.name);
|
||||
if (idx !== -1) {
|
||||
this.memories[idx] = memory;
|
||||
} else {
|
||||
this.memories.push(memory);
|
||||
}
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
remove(name: string): void {
|
||||
const idx = this.memories.findIndex(m => m.name === name);
|
||||
if (idx !== -1) {
|
||||
this.memories.splice(idx, 1);
|
||||
this.rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
rebuild(): void {
|
||||
this.tree = this.buildTree();
|
||||
}
|
||||
}
|
||||
|
||||
export type MemoryOptions = {
|
||||
/** Memory object */
|
||||
memory: Memory[] | MemoryCache;
|
||||
/** Inject N memories into the system prompt */
|
||||
inject?: boolean;
|
||||
/** expose recall tool to LLM */
|
||||
tool?: boolean;
|
||||
/** Update memory on compression */
|
||||
update?: boolean;
|
||||
/** Max context size of memories to inject to each call (removed immediately after use) */
|
||||
maxTokens?: number;
|
||||
}
|
||||
|
||||
export type Memory = {
|
||||
name: string;
|
||||
@@ -83,8 +220,6 @@ function getWeekSunday(monday: string): string {
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class MemoryManager {
|
||||
private pendingMemorizations = new Map<string, {
|
||||
memories: Memory[] | MemoryCache,
|
||||
@@ -128,6 +263,12 @@ export class MemoryManager {
|
||||
|
||||
constructor(private llm: any) {}
|
||||
|
||||
static normalize(m?: Memory[] | MemoryCache | MemoryOptions) {
|
||||
if(!m) return null;
|
||||
const raw = m instanceof MemoryCache || Array.isArray(m);
|
||||
return raw ? {memory: <Memory[] | MemoryCache>m, inject: true, tool: true, update: true} : {inject: true, tool: true, update: true, ...m};
|
||||
}
|
||||
|
||||
private async createTempMemory(conversation: string): Promise<Memory> {
|
||||
const timestamp = Date.now();
|
||||
const content = `---
|
||||
|
||||
Reference in New Issue
Block a user