Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 878a8794ee | |||
| 3f1289d993 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ztimson/ai-utils",
|
"name": "@ztimson/ai-utils",
|
||||||
"version": "1.4.2",
|
"version": "1.4.4",
|
||||||
"description": "AI Utility library",
|
"description": "AI Utility library",
|
||||||
"author": "Zak Timson",
|
"author": "Zak Timson",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
16
src/llm.ts
16
src/llm.ts
@@ -132,8 +132,8 @@ class LLM {
|
|||||||
return {
|
return {
|
||||||
name: toolName,
|
name: toolName,
|
||||||
description: `${a.delegate ? 'Delegate to ' : ''}Subagent: ${a.description || a.name}`,
|
description: `${a.delegate ? 'Delegate to ' : ''}Subagent: ${a.description || a.name}`,
|
||||||
args: <any>(a.delegate ? {} : {
|
args: <any>({
|
||||||
context: {type: 'string', description: 'Summary of related messages, samples, files, etc...', required: true},
|
context: !a.delegate ? {type: 'string', description: 'Summary of related messages, samples, files, etc...', required: true} : undefined,
|
||||||
instructions: {type: 'string', description: 'Detailed instructions for subagent to complete', required: true},
|
instructions: {type: 'string', description: 'Detailed instructions for subagent to complete', required: true},
|
||||||
}),
|
}),
|
||||||
fn: async (args: any, stream: any, ai: any, id?: string) => {
|
fn: async (args: any, stream: any, ai: any, id?: string) => {
|
||||||
@@ -148,8 +148,9 @@ class LLM {
|
|||||||
const q = a.delegate ? '' : `${args.instructions}${args.context ? `\n\n<context>${args.context}</context>` : ''}`;
|
const q = a.delegate ? '' : `${args.instructions}${args.context ? `\n\n<context>${args.context}</context>` : ''}`;
|
||||||
|
|
||||||
const request = this.ask(q, {
|
const request = this.ask(q, {
|
||||||
system: `You are a specialized subagent. ${a.delegate ? 'Your output streams directly to the user for the remainder of this turn. You are mid conversation - dispense with greetings.' : 'You are wrapped in a tool call that will be analysis by an LLM - dispense with conversation'}
|
system: `You are a specialized subagent being called from an orchestrator
|
||||||
As a subagent, focus on executing your task completely using available tools and returning only the final result - no commentary, questions, or dialogue.
|
${a.delegate ? 'Your output streams directly to the user for the remainder of this turn. You are mid conversation' : 'You are wrapped in a tool call that will be analysis by an LLM'}
|
||||||
|
Dispense with greetings and focus on your instructions using available tools and returning only the final result unless specifically instructed to converse
|
||||||
|
|
||||||
${a.system}`,
|
${a.system}`,
|
||||||
model: a.model || undefined,
|
model: a.model || undefined,
|
||||||
@@ -272,8 +273,6 @@ ${a.system}`,
|
|||||||
promise = (async () => {
|
promise = (async () => {
|
||||||
let tools: AiTool[] = options.tools || this.ai.options.llm?.tools || [];
|
let tools: AiTool[] = options.tools || this.ai.options.llm?.tools || [];
|
||||||
const prompts: string[] = [];
|
const prompts: string[] = [];
|
||||||
// `history` is the single source of truth from here on - mutated in place by
|
|
||||||
// this call AND by any nested/delegated agent calls sharing the same array
|
|
||||||
let history = options.history || [];
|
let history = options.history || [];
|
||||||
if(message) history.push({role: 'user', content: message, timestamp: Date.now()});
|
if(message) history.push({role: 'user', content: message, timestamp: Date.now()});
|
||||||
|
|
||||||
@@ -329,7 +328,8 @@ ${r.description}
|
|||||||
${r.content}
|
${r.content}
|
||||||
`).join('\n---\n')}
|
`).join('\n---\n')}
|
||||||
` : ''}${listed.length ? `
|
` : ''}${listed.length ? `
|
||||||
Also relevant but not preloaded (use \`memory_recall\`): ${listed.map(r => r.name).join(', ')}
|
Additional relevant memories (use \`memory_recall\`):
|
||||||
|
${listed.map(r => r.name).join(', ')}
|
||||||
` : ''}`.trim());
|
` : ''}`.trim());
|
||||||
}
|
}
|
||||||
if(mem.tool) tools.push(this.memoryManager.tools.read(mem.memory));
|
if(mem.tool) tools.push(this.memoryManager.tools.read(mem.memory));
|
||||||
@@ -344,8 +344,6 @@ Also relevant but not preloaded (use \`memory_recall\`): ${listed.map(r => r.nam
|
|||||||
if(aborted) throw Object.assign(new Error('Aborted'), {name: 'AbortError'});
|
if(aborted) throw Object.assign(new Error('Aborted'), {name: 'AbortError'});
|
||||||
|
|
||||||
prompts.unshift(options.system || this.ai.options.llm?.system || '');
|
prompts.unshift(options.system || this.ai.options.llm?.system || '');
|
||||||
// Message already appended to shared `history` above - pass '' so the provider
|
|
||||||
// doesn't push a duplicate user turn
|
|
||||||
request = this.models[m].ask('', {...options, tools, system: prompts.filter(Boolean).join('\n\n')});
|
request = this.models[m].ask('', {...options, tools, system: prompts.filter(Boolean).join('\n\n')});
|
||||||
let resp = await request;
|
let resp = await request;
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export class MemoryCache {
|
|||||||
|
|
||||||
add(memory: Memory): void {
|
add(memory: Memory): void {
|
||||||
this.memories.push(memory);
|
this.memories.push(memory);
|
||||||
|
rebuildGraph(this.memories);
|
||||||
this.rebuild();
|
this.rebuild();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +54,7 @@ export class MemoryCache {
|
|||||||
} else {
|
} else {
|
||||||
this.memories.push(memory);
|
this.memories.push(memory);
|
||||||
}
|
}
|
||||||
|
rebuildGraph(this.memories);
|
||||||
this.rebuild();
|
this.rebuild();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +62,7 @@ export class MemoryCache {
|
|||||||
const idx = this.memories.findIndex(m => m.name === name);
|
const idx = this.memories.findIndex(m => m.name === name);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
this.memories.splice(idx, 1);
|
this.memories.splice(idx, 1);
|
||||||
|
rebuildGraph(this.memories);
|
||||||
this.rebuild();
|
this.rebuild();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,6 +189,17 @@ export class MemoryManager {
|
|||||||
|
|
||||||
constructor(private llm: any) {}
|
constructor(private llm: any) {}
|
||||||
|
|
||||||
|
private ghostNodes(memories: Memory[]): string[] {
|
||||||
|
const names = new Set(memories.map(m => m.name));
|
||||||
|
const ghosts = new Set<string>();
|
||||||
|
for (const m of memories) {
|
||||||
|
for (const link of m.links) {
|
||||||
|
if (!names.has(link)) ghosts.add(link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...ghosts];
|
||||||
|
}
|
||||||
|
|
||||||
static normalize(m?: Memory[] | MemoryCache | MemoryOptions) {
|
static normalize(m?: Memory[] | MemoryCache | MemoryOptions) {
|
||||||
if(!m) return null;
|
if(!m) return null;
|
||||||
const raw = m instanceof MemoryCache || Array.isArray(m);
|
const raw = m instanceof MemoryCache || Array.isArray(m);
|
||||||
@@ -458,6 +472,8 @@ ${currentBody}
|
|||||||
|
|
||||||
private async factAgent(conversation: string, memories: Memory[], options: LLMRequest, weekKey: string): Promise<FactBucket[]> {
|
private async factAgent(conversation: string, memories: Memory[], options: LLMRequest, weekKey: string): Promise<FactBucket[]> {
|
||||||
const buckets = new Map<string, string[]>();
|
const buckets = new Map<string, string[]>();
|
||||||
|
const ghosts = this.ghostNodes(memories);
|
||||||
|
|
||||||
await this.llm.ask(conversation, {
|
await this.llm.ask(conversation, {
|
||||||
model: options.model,
|
model: options.model,
|
||||||
temperature: 0.2,
|
temperature: 0.2,
|
||||||
@@ -472,14 +488,15 @@ Rules:
|
|||||||
- If nothing worth remembering was said, do not call any tools
|
- If nothing worth remembering was said, do not call any tools
|
||||||
|
|
||||||
When extracting facts, you MUST also decide the exact destination path:
|
When extracting facts, you MUST also decide the exact destination path:
|
||||||
- Use an existing node name if the facts clearly belong there
|
- Reuse node names (including ghost) as much as possible IF the facts belongs there
|
||||||
- All information primarily about the user should go under "People/User"
|
- All information primarily about the user should go under "People/User"
|
||||||
- When required, create a new path following collection/subject format (e.g., People/Sarah, Projects/Oxide) — you are not limited to any fixed list of collections, use whatever fits
|
- When required, create a new path following collection/subject format (e.g., People/Sarah, Projects/Oxide) — you are not limited to any fixed list of collections, use whatever fits
|
||||||
- For journal entries, use "Journal"
|
- For journal entries, use "Journal"
|
||||||
|
|
||||||
Available nodes:
|
Available nodes:
|
||||||
- Journal
|
- Journal
|
||||||
${this.listNodes(memories).filter(n => !n.name.includes('Journal')).map(n => `- ${n.name}: ${n.description}`).join('\n') || 'None yet.'}`,
|
${this.listNodes(memories).filter(n => !n.name.includes('Journal')).map(n => `- ${n.name}: ${n.description}`).join('\n') || 'None yet.'}
|
||||||
|
${ghosts.length ? `${ghosts.map(g => `- ${g}: (Ghost)`).join('\n')}` : ''}`,
|
||||||
tools: [{
|
tools: [{
|
||||||
name: 'facts_extract',
|
name: 'facts_extract',
|
||||||
description: 'Submit facts with their destination',
|
description: 'Submit facts with their destination',
|
||||||
|
|||||||
Reference in New Issue
Block a user