@just-every/ensemble
Version:
LLM provider abstraction layer with unified streaming interface
376 lines • 14.7 kB
JavaScript
import { createToolFunction, ensembleRequest } from '../index.js';
import { v4 as uuid } from 'uuid';
export const agentToolCache = new Map();
export function exportAgent(agent, model) {
const agentExport = typeof agent.export === 'function'
? agent.export()
: {
agent_id: agent.agent_id,
name: agent.name,
model: agent.model,
modelClass: agent.modelClass,
parent_id: agent.parent_id,
cwd: agent.cwd,
};
if (model)
agentExport.model = model;
return agentExport;
}
export function getAgentSpecificTools(agent_id) {
const tools = [];
if (agentToolCache.has(agent_id)) {
tools.push(...agentToolCache.get(agent_id));
}
return tools;
}
export function cloneAgent(agent) {
const copy = Object.create(Object.getPrototypeOf(agent));
Object.entries(agent).forEach(([key, value]) => {
if (typeof value === 'function') {
copy[key] = value;
}
else if (key === 'parent' && value instanceof Agent) {
copy[key] = value;
}
else if (Array.isArray(value)) {
copy[key] = [...value];
}
else if (value && typeof value === 'object') {
copy[key] = { ...value };
}
else {
copy[key] = value;
}
});
return copy;
}
export class Agent {
agent_id;
name;
description;
instructions;
parent_id;
workers;
tools;
model;
modelClass;
modelSettings;
intelligence;
maxToolCalls;
maxToolCallRoundsPerTurn;
verifier;
maxVerificationAttempts;
args;
jsonSchema;
historyThread;
cwd;
modelScores;
disabledModels;
onToolCall;
processToolCall;
onToolResult;
onToolError;
onRequest;
onResponse;
onThinking;
onToolEvent;
params;
processParams;
constructor(definition) {
if (!definition || typeof definition !== 'object') {
throw new Error(`Agent constructor expects an AgentDefinition object, but received: ${typeof definition}`);
}
this.agent_id = definition.agent_id || uuid();
this.name = (definition.name || 'Agent').replaceAll(' ', '_');
this.description = definition.description;
this.instructions = definition.instructions;
this.tools = definition.tools || [];
this.model = definition.model;
this.modelClass = definition.modelClass;
this.jsonSchema = definition.jsonSchema;
this.params = definition.params;
this.modelSettings = definition.modelSettings || {};
this.maxToolCalls = definition.maxToolCalls ?? 200;
this.maxToolCallRoundsPerTurn = definition.maxToolCallRoundsPerTurn;
this.maxVerificationAttempts = definition.maxVerificationAttempts ?? 2;
this.processParams = definition.processParams;
this.historyThread = definition.historyThread;
this.cwd = definition.cwd;
if (definition.verifier) {
this.verifier = new Agent({
...definition.verifier,
verifier: undefined,
});
this.verifier.parent_id = this.agent_id;
}
this.onToolCall = definition.onToolCall;
this.onToolResult = definition.onToolResult;
this.onRequest = definition.onRequest;
this.onThinking = definition.onThinking;
this.onResponse = definition.onResponse;
this.onToolEvent = definition.onToolEvent;
if (this.jsonSchema) {
if (!this.modelSettings)
this.modelSettings = {};
this.modelSettings.json_schema = this.jsonSchema;
}
if (definition.workers) {
this.workers = definition.workers.map((createAgentFn) => {
return () => {
const agent = createAgentFn();
agent.parent_id = this.agent_id;
return agent;
};
});
this.tools = this.tools.concat(this.workers.map((createAgentFn) => {
const agent = createAgentFn();
agent.parent_id = this.agent_id;
if (this.onToolEvent) {
agent.onToolEvent = this.onToolEvent;
}
return agent.asTool();
}));
}
}
asTool() {
let description = `An agent called ${this.name}.\n\n${this.description}`;
if (this.tools) {
description += `\n\n${this.name} has access to the following tools:\n`;
this.tools.forEach(tool => {
description += `- ${tool.definition.function.name}\n`;
});
description += '\nUse this as a guide when to call the agent, but let the agent decide which tools to use.';
}
return createToolFunction(async (...args) => {
const agent = cloneAgent(this);
agent.agent_id = uuid();
agent.parent_id = this.parent_id;
if (this.onToolEvent) {
agent.onToolEvent = this.onToolEvent;
}
if (agent.processParams) {
let paramsObj;
if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
paramsObj = args[0];
}
else {
paramsObj = {};
const paramKeys = Object.keys(agent.params || {});
paramKeys.forEach((key, idx) => {
if (idx < args.length)
paramsObj[key] = args[idx];
});
}
const { prompt, intelligence } = await agent.processParams(agent, paramsObj);
return runAgentTool(agent, prompt, intelligence);
}
let task = typeof args[0] === 'string' ? args[0] : '';
let context = typeof args[1] === 'string' ? args[1] : undefined;
let warnings = typeof args[2] === 'string' ? args[2] : undefined;
let goal = typeof args[3] === 'string' ? args[3] : undefined;
let intelligence = args[4];
if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
const params = args[0];
task = params.task || task;
context = params.context || context;
warnings = params.warnings || warnings;
goal = params.goal || goal;
intelligence = params.intelligence || intelligence;
}
let prompt = `**Task:** ${task}`;
if (context) {
prompt += `\n\n**Context:** ${context}`;
}
if (warnings) {
prompt += `\n\n**Warnings:** ${warnings}`;
}
if (goal) {
prompt += `\n\n**Goal:** ${goal}`;
}
return runAgentTool(agent, prompt, intelligence);
}, description, this.params || {
task: {
type: 'string',
description: `What should ${this.name} work on? Generally you should leave the way the task is performed up to the agent unless the agent previously failed. Agents are expected to work mostly autonomously.`,
},
context: {
type: 'string',
description: `What else might the ${this.name} need to know? Explain why you are asking for this - summarize the task you were given or the project you are working on. Please make it comprehensive. A couple of paragraphs is ideal.`,
optional: true,
},
warnings: {
type: 'string',
description: `Is there anything the ${this.name} should avoid or be aware of? You can leave this as a blank string if there's nothing obvious.`,
optional: true,
},
goal: {
type: 'string',
description: `This is the final goal/output or result you expect from the task. Try to focus on the overall goal and allow the ${this.name} to make it's own decisions on how to get there. One sentence is ideal.`,
optional: true,
},
intelligence: {
type: 'string',
description: `What level of intelligence do you recommend for this task?
- low: (under 90 IQ) Mini model used.
- standard: (90 - 110 IQ)
- high: (110+ IQ) Reasoning used.`,
enum: ['low', 'standard', 'high'],
optional: true,
},
}, undefined, this.name);
}
async getTools() {
const combinedTools = new Map();
const baseTools = this.tools && this.tools.length > 0 ? this.tools : [];
for (const tool of baseTools) {
if (tool && tool.definition && tool.definition.function && tool.definition.function.name) {
const clonedTool = { ...tool };
clonedTool.definition = { ...tool.definition };
clonedTool.definition.function = {
...tool.definition.function,
};
clonedTool.definition.function.parameters = {
...tool.definition.function.parameters,
};
clonedTool.definition.function.parameters.properties = {
...tool.definition.function.parameters.properties,
};
await this.processDynamicToolParameters(clonedTool);
combinedTools.set(clonedTool.definition.function.name, clonedTool);
}
else {
console.warn('[Agent.getTools] Encountered a base tool with missing definition or name:', tool);
}
}
if (this.agent_id) {
const cachedAgentTools = getAgentSpecificTools(this.agent_id);
for (const tool of cachedAgentTools) {
if (tool && tool.definition && tool.definition.function && tool.definition.function.name) {
const clonedTool = { ...tool };
clonedTool.definition = { ...tool.definition };
clonedTool.definition.function = {
...tool.definition.function,
};
clonedTool.definition.function.parameters = {
...tool.definition.function.parameters,
};
clonedTool.definition.function.parameters.properties = {
...tool.definition.function.parameters.properties,
};
await this.processDynamicToolParameters(clonedTool);
combinedTools.set(clonedTool.definition.function.name, clonedTool);
}
else {
console.warn('[Agent.getTools] Encountered a cached tool with missing definition or name:', tool);
}
}
}
return Array.from(combinedTools.values());
}
async processDynamicToolParameters(tool) {
const properties = tool.definition.function.parameters.properties;
for (const paramName in properties) {
const param = properties[paramName];
if (typeof param.description === 'function') {
param.description = param.description();
}
if (typeof param.enum === 'function') {
param.enum = await param.enum();
}
if (param.properties) {
for (const nestedParamName in param.properties) {
const nestedParam = param.properties[nestedParamName];
if (typeof nestedParam.description === 'function') {
nestedParam.description = nestedParam.description();
}
if (typeof nestedParam.enum === 'function') {
nestedParam.enum = await nestedParam.enum();
}
}
}
if (param.items) {
const items = param.items;
if (typeof items.description === 'function') {
items.description = items.description();
}
if (typeof items.enum === 'function') {
items.enum = await items.enum();
}
}
}
}
export() {
const agentExport = {
agent_id: this.agent_id,
name: this.name,
};
if (this.model) {
agentExport.model = this.model;
}
if (this.modelClass) {
agentExport.modelClass = this.modelClass;
}
if (this.parent_id) {
agentExport.parent_id = this.parent_id;
}
if (this.cwd) {
agentExport.cwd = this.cwd;
}
return agentExport;
}
}
async function runAgentTool(agent, prompt, intelligence) {
agent.intelligence = intelligence || undefined;
const modelClass = agent.modelClass || 'standard';
switch (agent.intelligence) {
case 'low':
if (['standard'].includes(modelClass)) {
agent.modelClass = 'mini';
}
if (['code', 'reasoning'].includes(modelClass)) {
agent.modelClass = 'standard';
}
break;
case 'standard':
break;
case 'high':
if (['mini'].includes(modelClass)) {
agent.modelClass = 'standard';
}
if (['standard'].includes(modelClass)) {
agent.modelClass = 'reasoning';
}
break;
}
const messages = [];
messages.push({
type: 'message',
role: 'user',
content: prompt,
});
try {
const parentOnToolEvent = agent.onToolEvent;
const stream = ensembleRequest(messages, agent);
let fullResponse = '';
for await (const event of stream) {
if (parentOnToolEvent) {
await parentOnToolEvent(event);
}
if (event.type === 'message_complete' && 'content' in event) {
fullResponse = event.content;
}
}
return fullResponse;
}
catch (error) {
console.error(`Error in ${agent.name}: ${error}`);
return `Error in ${agent.name}: ${error}`;
}
}
export async function getToolsFromAgent(agent) {
if (agent && typeof agent.getTools === 'function') {
return await agent.getTools();
}
return agent?.tools || [];
}
//# sourceMappingURL=agent.js.map