Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d6debad86 | ||
|
|
6bed8f20b5 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ztimson/ai-utils",
|
"name": "@ztimson/ai-utils",
|
||||||
"version": "1.6.13",
|
"version": "1.7.1",
|
||||||
"description": "AI Utility library",
|
"description": "AI Utility library",
|
||||||
"author": "Zak Timson",
|
"author": "Zak Timson",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
+19
-13
@@ -19,6 +19,13 @@ const PDF_OCR_PAGE_THRESHOLD = 12; // above this many pages, OCR scanned pages i
|
|||||||
export type AnthropicConfig = {proto: 'anthropic', token: string | string[]};
|
export type AnthropicConfig = {proto: 'anthropic', token: string | string[]};
|
||||||
export type OpenAiConfig = {proto: 'openai', host?: string, token: string | string[]};
|
export type OpenAiConfig = {proto: 'openai', host?: string, token: string | string[]};
|
||||||
|
|
||||||
|
export type AgentRef = {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
delegate?: boolean;
|
||||||
|
fn: () => Agent | null | Promise<Agent | null>;
|
||||||
|
}
|
||||||
|
|
||||||
export type Agent = {
|
export type Agent = {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -29,7 +36,7 @@ export type Agent = {
|
|||||||
skills?: Skill[] | null;
|
skills?: Skill[] | null;
|
||||||
tools?: AiTool[] | null;
|
tools?: AiTool[] | null;
|
||||||
mcp?: McpServer[] | null;
|
mcp?: McpServer[] | null;
|
||||||
agents?: string[] | null;
|
agents?: AgentRef[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LLMFile = {
|
export type LLMFile = {
|
||||||
@@ -106,8 +113,8 @@ export type LLMRequest = {
|
|||||||
skills?: Skill[];
|
skills?: Skill[];
|
||||||
/** MCP servers to connect and expose as tools */
|
/** MCP servers to connect and expose as tools */
|
||||||
mcp?: McpServer[];
|
mcp?: McpServer[];
|
||||||
/** Subagents exposed as delegatable/wrapped tools */
|
/** Subagents exposed as delegatable/wrapped tools, resolved lazily via their `fn` */
|
||||||
agents?: Agent[];
|
agents?: AgentRef[];
|
||||||
/** Attach files to request */
|
/** Attach files to request */
|
||||||
files?: LLMFile[];
|
files?: LLMFile[];
|
||||||
/** @internal recursion guard for nested agent delegation */
|
/** @internal recursion guard for nested agent delegation */
|
||||||
@@ -265,22 +272,21 @@ class LLM {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupAgent(agents: Agent[] = [], allAgents: Agent[], history: LLMMessage[], aborts: ((keep?: boolean) => void)[], depth = 0, delegateState: {resp: string | null}): AiTool[] {
|
private setupAgent(stubs: AgentRef[] = [], history: LLMMessage[], aborts: ((keep?: boolean) => void)[], depth = 0, delegateState: {resp: string | null}): AiTool[] {
|
||||||
return agents.map(a => {
|
return stubs.map(stub => {
|
||||||
const toolName = `${a.delegate ? '' : 'sub'}agent_${snakeCase(a.name)}`;
|
const toolName = `${stub.delegate ? '' : 'sub'}agent_${snakeCase(stub.name)}`;
|
||||||
return {
|
return {
|
||||||
name: toolName,
|
name: toolName,
|
||||||
description: `${a.delegate ? 'Delegate to ' : ''}Subagent: ${a.description || a.name}`,
|
description: `${stub.delegate ? 'Delegate to ' : ''}Subagent: ${stub.description || stub.name}`,
|
||||||
args: clean<any>({
|
args: clean<any>({
|
||||||
context: !a.delegate ? {type: 'string', description: 'Summary of related messages, samples, files, etc...', required: true} : undefined,
|
context: !stub.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) => {
|
||||||
if(depth >= MAX_AGENT_DEPTH) return 'Max agent delegation depth exceeded';
|
if(depth >= MAX_AGENT_DEPTH) return 'Max agent delegation depth exceeded';
|
||||||
|
|
||||||
const nested = (a.agents || [])
|
const a = await stub.fn();
|
||||||
.map(name => allAgents.find(x => x.name === name))
|
if(!a) return `Agent "${stub.name}" could not be resolved`;
|
||||||
.filter((x): x is Agent => !!x && x.name !== a.name);
|
|
||||||
|
|
||||||
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>` : ''}`;
|
||||||
|
|
||||||
@@ -297,7 +303,7 @@ ${a.system}`,
|
|||||||
mcp: a.mcp || undefined,
|
mcp: a.mcp || undefined,
|
||||||
skills: a.skills || undefined,
|
skills: a.skills || undefined,
|
||||||
tools: a.tools || undefined,
|
tools: a.tools || undefined,
|
||||||
agents: nested,
|
agents: a.agents || [],
|
||||||
_agentDepth: depth + 1,
|
_agentDepth: depth + 1,
|
||||||
} as any);
|
} as any);
|
||||||
aborts.push(request.abort);
|
aborts.push(request.abort);
|
||||||
@@ -451,7 +457,7 @@ ${a.system}`,
|
|||||||
// Agents
|
// Agents
|
||||||
const agents = options.agents || this.ai.options?.llm?.agents;
|
const agents = options.agents || this.ai.options?.llm?.agents;
|
||||||
const delegateState: {resp: string | null} = {resp: null};
|
const delegateState: {resp: string | null} = {resp: null};
|
||||||
if(agents?.length) tools.push(...this.setupAgent(agents, agents, history, nestedAborts, options._agentDepth || 0, delegateState));
|
if(agents?.length) tools.push(...this.setupAgent(agents, history, nestedAborts, options._agentDepth || 0, delegateState));
|
||||||
|
|
||||||
// Memory
|
// Memory
|
||||||
const mem = MemoryManager.normalize(options.memory);
|
const mem = MemoryManager.normalize(options.memory);
|
||||||
|
|||||||
+7
-7
@@ -340,22 +340,22 @@ Think of this like an Obsidian vault with a clear division of responsibility:
|
|||||||
- A chronological, skimmable log of what actually happened: real discussions, decisions made, progress on projects, problems worked through
|
- A chronological, skimmable log of what actually happened: real discussions, decisions made, progress on projects, problems worked through
|
||||||
- This is NOT a transcript, and it is NOT a step-by-step record, its a compressed log of notable events & developments
|
- This is NOT a transcript, and it is NOT a step-by-step record, its a compressed log of notable events & developments
|
||||||
- One line per development is usually enough: what was worked on and the outcome, not the blow-by-blow of how
|
- One line per development is usually enough: what was worked on and the outcome, not the blow-by-blow of how
|
||||||
- Skip small talk and trivial exchanges entirely. Skip anything that's purely a todo item (goes in Todo Tasks) or a durable fact about a subject (goes in Entity Dossiers)
|
- Skip small talk and trivial exchanges entirely. Skip anything that's a todo item (goes in Todo Tasks) or a durable fact about a subject (goes in Entity Dossiers)
|
||||||
|
|
||||||
2. Todo Tasks
|
2. Todo Tasks
|
||||||
- Extract concrete tasks the user says need to be done, should be done, or were completed
|
- Extract concrete tasks the user says need to be done, should be done, or were completed
|
||||||
- Return the task text and whether it is still todo or is done
|
- Return the task text and whether it is still todo or is done
|
||||||
- A completed task should be marked done, not recreated as a new todo
|
- A completed task should be marked done, not recreated as a new todo
|
||||||
- Only extract actionable tasks, not general goals or observations
|
- Only extract actionable tasks, not general goals or observations, if none - omit returning a tasks array
|
||||||
- Assign each task a subject:
|
- Assign each task a subject:
|
||||||
- If the task belongs to a persistent entity (a project, a class, etc.), use that entity's exact node name, or a new entity path if it doesn't exist yet
|
- If the task belongs to a persistent entity (a project, a class, etc.), use that entity's exact node name, or a new entity path if it doesn't exist yet
|
||||||
- If it's a personal/life task with no entity of its own (reach out to someone, reply to an email, pay a bill, etc.), leave subject as an empty string — it belongs in the journal, not a new document
|
- If it's a personal/life task with no entity of its own (reach out to someone, reply to an email, pay a bill, etc.), leave subject as an empty string — it belongs in the journal, not a new document
|
||||||
|
|
||||||
3. Entity Dossiers
|
3. Entity Dossiers
|
||||||
- Detailed dossiers with all information regarding a subject
|
- Detailed dossiers with all factual information regarding a subject
|
||||||
- Record the final/end state, not intermediate changes
|
- Record the final/end state, not intermediate changes
|
||||||
- Ignore assistant claims, guesses, greetings, or temporary details
|
- Ignore assistant claims, guesses, greetings, or temporary details
|
||||||
- NEVER create a document for something that's only meaningful as a point in time — a single conversation, a one-off decision, a debugging session, a date. That's a journal entry, not an entity
|
- NEVER create a dossier for something I wouldnt find in a wiki site: temporary information, debugging, guesses, conversations (this is all journal entry stuff!)
|
||||||
- identify its HOME ENTITY:
|
- identify its HOME ENTITY:
|
||||||
- The HOME ENTITY name should always be a [abstract|pro]noun
|
- The HOME ENTITY name should always be a [abstract|pro]noun
|
||||||
- The grammatical subject/owner of the fact is the strongest clue
|
- The grammatical subject/owner of the fact is the strongest clue
|
||||||
@@ -379,9 +379,9 @@ Available nodes:
|
|||||||
${this.listNodes(store.list).map(n => `- ${n.name}: ${n.description}`).join('\n') || 'None yet.'}
|
${this.listNodes(store.list).map(n => `- ${n.name}: ${n.description}`).join('\n') || 'None yet.'}
|
||||||
${ghosts.length ? `${ghosts.map(g => `- ${g}: (Ghost)`).join('\n')}` : ''}`,
|
${ghosts.length ? `${ghosts.map(g => `- ${g}: (Ghost)`).join('\n')}` : ''}`,
|
||||||
schema: {
|
schema: {
|
||||||
journal: {type: 'string', description: 'Short day-to-day recap; empty if nothing happened.', required: false},
|
journal: {type: 'string', description: 'Short bullet point recap, omit if nothing notable happened'},
|
||||||
tasks: {
|
tasks: {
|
||||||
type: 'array', description: 'Concrete tasks mentioned or completed in the conversation.', required: false, items: {
|
type: 'array', description: 'Concrete tasks mentioned or completed in the conversation, omit if none', items: {
|
||||||
type: 'object', items: {
|
type: 'object', items: {
|
||||||
subject: {type: 'string', description: 'Exact node name / new persistent entity path this task belongs to, or an empty string if this is a personal task with no entity of its own (those go in the journal)', required: true},
|
subject: {type: 'string', description: 'Exact node name / new persistent entity path this task belongs to, or an empty string if this is a personal task with no entity of its own (those go in the journal)', required: true},
|
||||||
task: {type: 'string', description: 'Concise actionable task', required: true},
|
task: {type: 'string', description: 'Concise actionable task', required: true},
|
||||||
@@ -390,7 +390,7 @@ ${ghosts.length ? `${ghosts.map(g => `- ${g}: (Ghost)`).join('\n')}` : ''}`,
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
buckets: {
|
buckets: {
|
||||||
type: 'array', description: 'Groups of facts to remember; empty array if nothing worth storing.', items: {
|
type: 'array', description: 'Groups of facts to remember; omit if none', items: {
|
||||||
type: 'object', items: {
|
type: 'object', items: {
|
||||||
subject: {type: 'string', description: 'Exact node name or new persistent entity path', required: true},
|
subject: {type: 'string', description: 'Exact node name or new persistent entity path', required: true},
|
||||||
facts: {type: 'array', description: 'Facts to store here', items: {type: 'string'}},
|
facts: {type: 'array', description: 'Facts to store here', items: {type: 'string'}},
|
||||||
|
|||||||
Reference in New Issue
Block a user