loom-agents
Version:
A lightweight, composable framework for building hierarchical AI agent systems using OpenAI's API.
544 lines • 24.2 kB
JavaScript
import { Loom } from "../Loom/Loom.js";
import { v4 } from "uuid";
import OpenAI from "openai";
const valueToString = (value) => {
if (value === null || value === undefined) {
return String(value);
}
if (typeof value === "object") {
try {
return JSON.stringify(value);
}
catch (error) {
return `[Object: Serialization Error - ${error.message}]`;
}
}
return String(value);
};
export class Agent {
uuid;
config;
defaultModel = "gpt-4o";
defaultTimeout = 60000;
_client;
_api;
constructor(config) {
if (!config.purpose)
throw new Error("Agent purpose is required");
if (!config.name)
throw new Error("Agent name is required");
if (config.client_config)
this._client = new OpenAI(config.client_config);
if (config.api)
this._api = config.api;
this.uuid = `agent.${v4()}`;
this.config = {
...config,
model: config.model || this.defaultModel,
timeout_ms: config.timeout_ms || this.defaultTimeout,
};
}
get client() {
if (!this._client)
return Loom.openai;
return this._client;
}
async prepareTools() {
const toolsArray = [];
if (this.config.mcp_servers) {
for (const mcp of this.config.mcp_servers) {
const server_tools = await mcp.getTools();
for (const tool of server_tools.tools) {
toolsArray.push({
type: "function",
name: `mcp_${tool.name}`,
description: `MCP Tool: ${tool.name}`,
parameters: {
type: "object",
properties: tool.inputSchema.properties,
required: tool.inputSchema.required,
additionalProperties: false,
},
strict: true,
});
}
}
}
if (this.config.tools && this.config.tools.length > 0) {
toolsArray.push(...this.config.tools.map((tool) => ({
type: "function",
name: tool.name,
description: tool.description,
parameters: {
type: "object",
properties: tool.parameters,
required: Object.keys(tool.parameters),
additionalProperties: false,
},
strict: true,
})));
}
if (this.config.sub_agents && this.config.sub_agents.length > 0) {
toolsArray.push({
type: "function",
name: "CallSubAgent",
description: "Pass an input to a sub agent for processing and return the output",
parameters: {
type: "object",
properties: {
sub_agent: {
type: "string",
description: "The name of the sub agent to call",
enum: this.config.sub_agents.map((agent) => agent.config.name),
},
request: {
type: "string",
description: "The request to send to the sub agent",
},
},
required: ["sub_agent", "request"],
additionalProperties: false,
},
strict: true,
});
}
if (this.config.web_search?.enabled) {
toolsArray.push({
type: "web_search_preview",
...this.config.web_search.config,
});
}
return toolsArray;
}
ToolsToCompletionTools(tools) {
if (!tools || tools.length === 0)
return undefined;
const toolsArray = [];
toolsArray.push(...tools
.filter((tool) => tool.type === "function")
.map((tool) => {
return {
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
strict: tool.strict,
},
};
}));
return toolsArray;
}
async run_completions(input, trace) {
const run_trace = trace?.start("run_completions", {});
const context = typeof input === "string"
? [{ content: input, role: "user" }]
: input.context;
const response = await this.client.chat.completions.create({
model: this.config.model,
// metadata: {
// loom: "powered",
// agent: this.config.name,
// },
// store: true, // meta data isn't enabled unless you store responses, so.. TODO: Completion mode add `store` config variable
messages: [
{
role: "system",
content: `You are an AI Agent, your purpose is to (${this.config.purpose}). ${this.config.sub_agents && this.config.sub_agents.length > 0
? `You can query the following 'sub_agents' with the 'CallSubAgent' tool: {${this.config.sub_agents
.map((agent) => `${agent.config.name}`)
.join(", ")}}`
: ""} Consider using all the tools available to you to achieve this. Start acting immediately.`,
},
...context,
],
tools: this.ToolsToCompletionTools(await this.prepareTools()),
web_search_options: this.config.web_search?.enabled
? {
search_context_size: this.config.web_search.config?.search_context_size,
user_location: {
type: this.config.web_search.config?.user_location
?.type,
approximate: {
city: this.config.web_search.config?.user_location?.city,
country: this.config.web_search.config?.user_location?.country,
region: this.config.web_search.config?.user_location?.region,
},
},
}
: undefined,
});
const hasToolCalls = response.choices.some((item) => item.finish_reason === "function_call" ||
item.finish_reason === "tool_calls");
if (response.choices[0].finish_reason === "stop" && !hasToolCalls) {
return {
status: "completed",
final_message: response.choices[0].message.content,
context: [
...context,
...response.choices.map((choice) => choice.message),
],
};
}
if (response.choices[0].finish_reason === "content_filter") {
return {
status: "error",
final_message: "[Content Filter] " + response.choices[0].message.content,
context: [
...context,
...response.choices.map((choice) => choice.message),
],
};
}
if (response.choices[0].finish_reason === "length") {
return {
status: "error",
final_message: "[Length] " + response.choices[0].message.content,
context: [
...context,
...response.choices.map((choice) => choice.message),
],
};
}
if (response.choices[0].finish_reason === "function_call") {
return {
status: "error",
final_message: "[Function Call] Not implemented",
context: [
...context,
...response.choices.map((choice) => choice.message),
],
};
}
const call_results = [];
if (response.choices[0].finish_reason === "tool_calls") {
const tool_calls = response.choices[0].message.tool_calls;
if (tool_calls && tool_calls.length > 0) {
for (const tool_call of tool_calls) {
if (tool_call.function.name.startsWith("mcp_")) {
trace?.start("mcp_tool_call", {
tool_call,
});
const mcp_tool_name = tool_call.function.name.replace("mcp_", "");
const mcpResult = await this.config.mcp_servers?.reduce(async (accPromise, server) => {
const acc = await accPromise;
if (acc.mcp && acc.tool)
return acc;
const { tools } = await server.getTools();
const tool = tools.find((tool) => tool.name === mcp_tool_name);
return tool ? { mcp: server, tool } : acc;
}, Promise.resolve({
mcp: undefined,
tool: undefined,
}));
const { mcp, tool } = mcpResult || {};
if (!mcp || !tool) {
call_results.push({
role: "tool",
tool_call_id: tool_call.id,
content: `[MCP Tool Call Error] ${mcp_tool_name} - Tool not found`,
});
continue;
}
try {
const result = await mcp.callTool({
name: mcp_tool_name,
arguments: JSON.parse(tool_call.function.arguments),
});
if (result.isError) {
call_results.push({
role: "tool",
tool_call_id: tool_call.id,
content: `[MCP Tool Call Error] ${mcp_tool_name} - ${valueToString(result.content)}`,
});
continue;
}
call_results.push({
role: "tool",
tool_call_id: tool_call.id,
content: valueToString(result.content),
});
}
catch (error) {
call_results.push({
role: "tool",
tool_call_id: tool_call.id,
content: `[MCP Tool Call Error] ${mcp_tool_name} - ${error.message}`,
});
}
finally {
}
}
else if (tool_call.function.name === "CallSubAgent") {
trace?.start("call_sub_agent", {
tool_call,
});
const args = JSON.parse(tool_call.function.arguments);
const sub_agent = this.config.sub_agents?.find((agent) => agent.config.name === args.sub_agent);
if (!sub_agent) {
call_results.push({
role: "tool",
tool_call_id: tool_call.id,
content: `[Sub Agent Error] ${args.sub_agent} - Sub Agent not found`,
});
continue;
}
const result = await sub_agent.run({
context: [
...context,
{
role: "user",
content: args.request,
},
],
}, trace);
call_results.push({
role: "tool",
tool_call_id: tool_call.id,
content: result.final_message,
});
}
else {
trace?.start("tool_call", {
tool_call,
});
const tool = this.config.tools?.find((tool) => tool.name === tool_call.function.name);
if (!tool) {
call_results.push({
role: "tool",
tool_call_id: tool_call.id,
content: `[Tool Call Error] ${tool_call.function.name} - Tool not found`,
});
continue;
}
try {
const result = await tool.callback(JSON.parse(tool_call.function.arguments));
call_results.push({
role: "tool",
tool_call_id: tool_call.id,
content: valueToString(result),
});
}
catch (error) {
call_results.push({
role: "tool",
tool_call_id: tool_call.id,
content: `[Tool Call Error] ${tool_call.function.name} - ${error.message}`,
});
}
finally {
}
}
}
}
}
return this.run_completions({
context: [
...context,
...response.choices.map((choice) => choice.message),
...call_results,
],
}, trace).finally(() => { });
}
async run_responses(input, trace) {
const run_trace = trace?.start("run_responses", {});
const context = typeof input === "string"
? [{ content: input, role: "user" }]
: input.context;
const response = await this.client.responses.create({
model: this.config.model,
metadata: {
loom: "powered",
agent: this.config.name,
},
input: [
{
role: "system",
content: `You are an AI Agent, your purpose is to (${this.config.purpose}). ${this.config.sub_agents && this.config.sub_agents.length > 0
? `You can query the following 'sub_agents' with the 'CallSubAgent' tool: {${this.config.sub_agents
.map((agent) => `${agent.config.name}`)
.join(", ")}}`
: ""} Consider using all the tools available to you to achieve this. Start acting immediately.`,
},
...context,
],
tools: await this.prepareTools(),
});
const hasToolCalls = response.output.some((item) => item.type === "function_call");
if (response.status === "completed" &&
response.output_text &&
!hasToolCalls) {
return {
status: "completed",
final_message: response.output_text || "[Unknown] Something went wrong",
context: [...context, ...response.output],
};
}
if (response.status === "failed") {
return {
status: "error",
final_message: `[Failed] ${response.output[0].content} ${response.error?.message}`,
context: [...context, ...response.output],
};
}
if (response.status === "incomplete") {
return {
status: "error",
final_message: `[Incomplete] ${response.output[0].content} ${response.incomplete_details?.reason}`,
context: [...context, ...response.output],
};
}
const call_results = [];
if (response.output[0].type === "function_call") {
const tool_calls = response.output;
if (tool_calls && tool_calls.length > 0) {
for (const tool_call of tool_calls) {
if (tool_call.name.startsWith("mcp_")) {
trace?.start("mcp_tool_call", {
tool_call,
});
const mcp_tool_name = tool_call.name.replace("mcp_", "");
const mcpResult = await this.config.mcp_servers?.reduce(async (accPromise, server) => {
const acc = await accPromise;
if (acc.mcp && acc.tool)
return acc;
const { tools } = await server.getTools();
const tool = tools.find((tool) => tool.name === mcp_tool_name);
return tool ? { mcp: server, tool } : acc;
}, Promise.resolve({
mcp: undefined,
tool: undefined,
}));
const { mcp, tool } = mcpResult || {};
if (!mcp || !tool) {
call_results.push({
type: "function_call_output",
call_id: tool_call.call_id,
output: `[MCP Tool Call Error] ${mcp_tool_name} - Tool not found`,
});
continue;
}
try {
const result = await mcp.callTool({
name: mcp_tool_name,
arguments: JSON.parse(tool_call.arguments),
});
if (result.isError) {
call_results.push({
type: "function_call_output",
call_id: tool_call.call_id,
output: `[MCP Tool Call Error] ${mcp_tool_name} - ${valueToString(result.content)}`,
});
continue;
}
call_results.push({
type: "function_call_output",
call_id: tool_call.call_id,
output: valueToString(result.content),
});
}
catch (error) {
call_results.push({
type: "function_call_output",
call_id: tool_call.call_id,
output: `[MCP Tool Call Error] ${mcp_tool_name} - ${error.message}`,
});
}
finally {
}
}
else if (tool_call.name === "CallSubAgent") {
trace?.start("call_sub_agent", {
tool_call,
});
const args = JSON.parse(tool_call.arguments);
const sub_agent = this.config.sub_agents?.find((agent) => agent.config.name === args.sub_agent);
if (!sub_agent) {
call_results.push({
type: "function_call_output",
call_id: tool_call.call_id,
output: `[Sub Agent Error] ${args.sub_agent} - Sub Agent not found`,
});
continue;
}
const result = await sub_agent.run_responses({
context: [
...context,
{
role: "user",
content: args.request,
},
],
}, trace);
call_results.push({
type: "function_call_output",
call_id: tool_call.call_id,
output: result.final_message,
});
}
else {
trace?.start("tool_call", {
tool_call,
});
const tool = this.config.tools?.find((tool) => tool.name === tool_call.name);
if (!tool) {
call_results.push({
type: "function_call_output",
call_id: tool_call.call_id,
output: `[Tool Call Error] ${tool_call.name} - Tool not found`,
});
continue;
}
try {
const result = await tool.callback(JSON.parse(tool_call.arguments));
call_results.push({
type: "function_call_output",
call_id: tool_call.call_id,
output: valueToString(result),
});
}
catch (error) {
call_results.push({
type: "function_call_output",
call_id: tool_call.call_id,
output: `[Tool Call Error] ${tool_call.name} - ${error.message}`,
});
}
finally {
}
}
}
}
}
return this.run_responses({
context: [...context, ...response.output, ...call_results],
}, trace).finally(() => { });
}
async run(input, trace) {
const content = typeof input === "string"
? [{ content: input, role: "user" }]
: input.context;
const api = this._api ? this._api : Loom.api;
if (api === "responses")
return this.run_responses({ context: content }, trace);
return this.run_completions({ context: content }, trace);
}
asTool(parameters) {
return {
name: this.config.name.replace(/[^a-zA-Z0-9_-]/g, ""),
parameters: parameters
? parameters
: {
request: {
type: "string",
description: `Request to send to the ${this.config.name.replace(/[^a-zA-Z0-9_-]/g, "")} agent`,
},
},
callback: async (...args) => {
return await this.run(`You were invoked as a tool with the following request - ${JSON.stringify(args)}`);
},
description: this.config.purpose,
};
}
}
//# sourceMappingURL=Agent.js.map