So you’ve decided to build an AI agent. That’s an exciting first step into a rapidly evolving field. While there’s no shortage of impressive tools and deep dive content available, you’ll quickly discover that navigating through the maze of offerings can be overwhelming. What started as an inspiring project can swiftly turn into a dizzying journey through countless technological rabbit holes.
The landscape is filled with compelling platforms and frameworks for building agents. However, a common pattern emerges: many of these platforms are best at creating polished demos that run smoothly on your laptop. Their developer experience (DevEx) shines in getting you from zero to one, showcasing quick wins and immediate results. But when faced with real-world scaling challenges, these same platforms often become unwieldy. The initial simplicity that made them attractive transforms into technical debt, and what once facilitated rapid development becomes a bottleneck in resolving critical issues.
Our philosophy takes a different approach. Instead of constraining developers with opinionated frameworks, we believe in empowering them with fundamental building blocks. By providing core components that handle the essential yet time-consuming aspects of AI agent development, we enable developers to focus on what matters most: building robust, scalable solutions tailored to their specific needs.
In this blog, we’ll describe the essential architecture and components of an AI agent, demonstrating how to build one using LiquidMetal’s Raindrop. While we’ve chosen our platform for this demonstration, it’s worth noting that the code we’ll write is pure TypeScript, making it platform-agnostic and readily adaptable to your preferred environment. Whether you’re building on our platform or incorporating these concepts into your own stack, the principles and patterns we’ll discuss remain universally applicable.
An AI agent is a sophisticated software system that combines artificial intelligence with autonomous decision-making capabilities. Unlike traditional software that simply follows predefined rules, AI agents can understand, reason, plan, and learn from their interactions, making them more adaptable and capable of handling complex tasks.
At their core, AI agents typically consist of several key components:
We will discuss each component in more detail in the sections below.
At the core of every AI agent lies the agent loop - a process that drives decision-making and action execution. While implementations can vary significantly in complexity, most basic structures consists of two primary steps that repeat until either the task is completed or the maximum reasoning depth is reached: reason and act [1][Yao et. all.].
During the reasoning phase, the Large Language Model (LLM) serves as the agent’s brain, evaluating all available information to determine the optimal next step. This phase involves analyzing the current context, understanding the task requirements, and planning the most effective course of action.
In our implementation the LLM might decide to:
It is generally recommended to limit the agent loop depth to avoid trapping it in an endless loop
Following the reasoning phase, the agent executes the chosen action. This could involve retrieving specific information from memory, utilizing one or more tools, or synthesizing a final response if the agent determines it has gathered all necessary information and completed all required actions.
The diagram above illustrates a high-level overview of an agent loop, depicting how it cycles through reasoning and action phases. To better understand this process in practice, let’s look at how an agent processes a common question through multiple iterations.
Assume the system is presented with the following input query: “What is the capital of France and what is the weather in this city right now?”
This question, requires multiple processing steps for an agent to ensure accuracy.
The first loop begins with a reasoning phase where the agent evaluates the initial component of the question - identifying France’s capital. Despite language models often containing this information in their training data, the agent follows its directive to verify facts through authorized sources. The reasoning phase concludes that querying the semantic knowledge endpoint will provide the most reliable answer. In the act phase the agent pulls the relevant info from the semantic endpoint.
Moving into the second loop, the agent has confirmed Paris as France’s capital through the semantic endpoint. The reasoning phase now focuses on the weather component of the question. Recognizing that current weather data requires real-time information, the agent determines it must utilize the weather tool, passing “Paris” as the location parameter.
The third loop brings all pieces together. With both the verified capital information and current weather data in hand, the reasoning phase determines no additional information is needed. The agent synthesizes its gathered data into a complete response: “The capital of France is Paris and the current weather is 72F.”
This example demonstrates the basics of the agent loop architecture. Each loop builds upon previous knowledge, creating a reliable path from question to answer through verified data and structured reasoning.
In the following sections we are taking a deep dive into an agentic AI implementation in typescript. In this example we are creating an AI agent that has access to a semantic knowledge endpoint, various tools such as weather and news API’s and memory stores.
First we implement the main agent loop including the reasoning and acting phase. This is built around a TypeScript method called processInput that takes two parameters: the user’s input as a string and an optional maximum depth limit (defaulting to 10 iterations).
The method starts by maintaining a conversation context array, adding each user input as it arrives. It then enters a while loop that continues until either we reach a final answer or hit our maximum depth limit. This depth limit is crucial for production systems - without it, complex queries could potentially trap the agent in endless loops.
Inside the main loop, we make an API call to Groq’s LLM service. The API call includes two key elements: a system message that defines our agent’s capabilities and response format requirements, and a user message containing the current conversation context. We deliberately set the response format to json_object, forcing the model to provide structured output that our code can reliably parse and act upon.
The system message defines our agent’s interface with its tools. For each tool - whether it’s checking the weather, fetching news, or accessing memory systems - we specify exact JSON schemas for the required arguments. For example, the weather tool expects a city name and optional country/state codes, while the news tool needs a search query and optional date range. This strict typing helps prevent runtime errors from malformed tool calls.
After receiving the LLM’s response, our code handles three possible paths:
The error handling is particularly thorough given the nondetermenistic nature of LLMs - we wrap our JSON parsing in try-catch blocks and include detailed logging statements. This makes debugging easier in production environments where we can’t directly observe the agent’s behavior.
When the loop concludes - either through finding an answer or hitting the depth limit - the final response gets added to our conversation context before being returned. This maintains a complete record of the interaction, which becomes valuable context for future queries to the same agent instance.
While we strictly control the format of our LLM’s responses, we place no restrictions on which tools it can use or in what order. This allows the agent to develop complex, multi-step solutions while preventing the most common failure modes in agentic systems.
/**
* Main processing function that handles user input and generates responses.
* This implements the core reasoning loop:
* 1. Add user input to conversation history
* 2. Ask Groq LLM to reason about what's needed
* 3. Either use a tool (weather, news) or provide final answer
* 4. Repeat until we have an answer or hit max iterations
*
* @param userInput - The text input from the user
* @param maxDepth - Maximum number of reasoning iterations (default 10)
* @returns A string containing the agent's final response
*/
async processInput(userInput: string, maxDepth: number = 10): Promise<string> {
// Add user input to conversation history
this.context.push(`User: ${userInput}`);
console.log("in processInput");
// Track whether we've reached a final answer
let isComplete = false;
let finalAnswer = '';
let currentDepth = 0;
// Main reasoning loop - continue until we have an answer or hit max depth
while (!isComplete && currentDepth < maxDepth) {
console.log(`Processing iteration ${currentDepth} of ${maxDepth}`);
// Call Groq API to get the next reasoning step
// We use their llama-3.1-70b model which is optimized for reasoning
const reasoningResponse = await fetch('https://api.groq.com/openai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.env.GROQ_API_KEY}`
},
body: JSON.stringify({
model: 'llama-3.1-70b-versatile',
messages: [{
role: 'system',
content: `You are an AI assistant that always responds in JSON format. You have access to these tools:
- weather: Get the current weather for a location. Requires arguments in format:
{
"city": string (required),
"country": string (optional, two-letter country code),
"state": string (optional, state code for US locations)
}
Example: { "city": "London" } or { "city": "New York", "state": "NY", "country": "US" }
- news: Retrieve news articles based on a search query. Requires arguments in format:
{
"query": string (required),
"fromDate": string (optional, in YYYY-MM-DD format)
}
Example: { "query": "technology", "fromDate": "2023-10-01" }
Set date to one week ago (${new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]}) if no date is provided
- short_term_memory: Access to current conversation context
- long_term_memory: Access to past conversations
For each user request, analyze:
1. What information is needed to answer?
2. What tools (if any) should be used?
3. Is enough information available to answer?
IMPORTANT: After using a tool, evaluate the information received. If you have enough information, provide a final answer. If not, consider using a different tool. Do not use the same tool repeatedly with the same arguments.
Your response must follow this exact JSON schema:
{
"thought": "string (your reasoning about what's needed)",
"action": "string (either 'use_tool' or 'provide_answer')",
"tool": "string (tool name if action is use_tool, null otherwise)",
"arguments": object | null (see tool descriptions for required formats),
"answer": "string (final answer if action is provide_answer, null otherwise). In your answer, include code in a code block if any was generated"
}`
}, {
role: 'user',
content: `Current context:\n${this.context.join('\n')}\n\nWhat should be done next?`
}],
temperature: 0.7,
response_format: { type: "json_object" } // Force JSON output
})
});
// Get response as text for logging/debugging
const responseBody = await reasoningResponse.text();
console.log('API Response:', responseBody);
try {
// Parse the nested JSON responses
const reasoningData = JSON.parse(responseBody);
const reasoning = JSON.parse(reasoningData.choices[0].message.content);
// Handle the AI's decision - either provide final answer or use a tool
if (reasoning.action === 'provide_answer') {
// We have enough information - store the answer and complete
finalAnswer = reasoning.answer;
isComplete = true;
} else if (reasoning.action === 'use_tool') {
// We need more information - call the appropriate tool
const toolResult = await this.useTool(reasoning.tool, reasoning.arguments);
// If the tool returned useful information, add it to context
if (toolResult.success && toolResult.data.content) {
this.context.push(`Tool Result (${reasoning.tool}):
Information received:
${JSON.stringify(toolResult.data.content, null, 2)}
What would you like to do with this information?`);
} else {
// Tool failed - log error and add failure note to context
console.error('Tool did not return useful information:', toolResult.error);
this.context.push(`Tool (${reasoning.tool}) failed to provide useful information.`);
}
}
} catch (error) {
// Handle any JSON parsing errors
console.error('JSON Parsing Error:', error);
throw error;
}
currentDepth++;
}
// If we hit max depth without completing, return an error message
if (!isComplete) {
finalAnswer = `I apologize, but I was unable to reach a conclusion within the maximum allowed ${maxDepth} reasoning steps.`;
}
// Add the final answer to context and return it
this.context.push(`Assistant: ${finalAnswer}`);
return finalAnswer;
}
Tools are the fundamental components that enable AI agents to interact with the external world and perform concrete actions. While the language model serves as the agent’s reasoning engine, tools provide the actual capabilities to execute specific tasks, access external systems, and manipulate data in meaningful ways. For tools to be usable by the AI, we need to provide clear definitions of their required parameters during the acting phase of our agent loop.
In our implementation, we’ve equipped the agent with access to a weather API, a news API, and memory components. The memory implementation is currently a placeholder to keep this example straightforward - in a production environment, you’ll want to replace these with more robust memory retrieval components.
The tool selection mechanism itself is straightforward, implemented through a switch statement that processes the output from the agent’s reasoning phase. The selected tool executes its function and returns the gathered information back to the main agent loop, where it becomes input for the next reasoning step. Here’s the implementation:
/**
* Tool handling function that provides access to different capabilities.
* Available tools:
* - weather: Get current weather for a location using OpenWeatherMap API
* - news: Get news articles based on search query using NewsAPI
* - short_term_memory: Access current conversation context
* - long_term_memory: Access historical conversations (currently mocked)
*
* @param tool - Name of the tool to use
* @param args - Arguments to pass to the tool
* @returns ToolResponse containing success/failure and any returned data
*/
private async useTool(tool: string, args: any): Promise<ToolResponse> {
switch (tool) {
// Get news articles based on a search query
case 'news':
console.log("in news tool");
const { query, fromDate } = args;
try {
const newsResponse = await fetch(
`https://newsapi.org/v2/everything?` +
`q=${encodeURIComponent(query)}` +
`${fromDate ? `&from=${fromDate}` : ''}` +
`&sortBy=popularity` +
`&apiKey=${this.env.NEWS_API_KEY}`,
{
headers: {
'User-Agent': 'LiquidMetal AI Agent/1.0',
'Content-Type': 'application/json'
}
}
);
if (!newsResponse.ok) {
console.error('News API error:', await newsResponse.text());
return {
success: false,
data: null,
error: `Failed to fetch news: ${newsResponse.status} ${newsResponse.statusText}`
};
}
const newsData = await newsResponse.json();
// Check if we got any articles
if (!newsData.articles || newsData.articles.length === 0) {
return {
success: false,
data: null,
error: 'No news articles found'
};
}
return {
success: true,
data: {
type: 'news',
content: {
articles: newsData.articles.slice(0, 5) // Only return top 5 articles
}
}
};
} catch (error) {
console.error('News API error:', error);
return {
success: false,
data: null,
error: `Failed to fetch news: ${error.message}`
};
}
// Get the current weather in a given city using OpenWeatherMap API
case 'weather':
console.log("in weather tool");
const { city, country = '', state = '' } = args;
const location = [city, state, country].filter(Boolean).join(',');
const weatherResponse = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${location}&appid=${this.env.WEATHER_API_KEY}`
);
if (!weatherResponse.ok) {
return {
success: false,
data: null,
error: 'Location not found'
};
}
const weatherData = await weatherResponse.json();
return {
success: true,
data: {
type: 'weather',
content: weatherData
}
};
// Access the current conversation history stored in context
case 'short_term_memory':
console.log("in short term memory tool");
return {
success: true,
data: { type: 'conversation', content: this.context }
};
// Access past conversations (currently returns mock data)
case 'long_term_memory':
console.log("in long term memory tool");
return {
success: true,
data: { type: 'past_conversations', content: 'There are no long term memories yet' }
};
// Handle unknown tool requests
default:
return {
success: false,
data: null,
error: 'Unknown tool'
};
}
}
The Raindrop manifest provides an easy way to deploy our agent application on the Raindrop platform. Think of this manifest as a blueprint that defines your application’s structure and its required components. For this demonstration, we’re keeping things minimal - we only need to specify a single service and the API keys required for authentication.
The manifest below defines our application named “agent-template”. It specifies three secret environment variables for our API keys (Groq, Weather, and News) and sets up a public-facing service that will host our agent. The service configuration includes a domain specification where your agent will be accessible.
application "agent-template" {
// You need to set all the required API keys using the CLI
// raindrop build env set agent-template:env:<API_KEY_NAME> <API KEY>
// groq api key
env "GROQ_API_KEY" {
secret = true
}
// weather API
env "WEATHER_API_KEY" {
secret = true
}
// news API
env "NEWS_API_KEY" {
secret = true
}
// agent service
service "agent" {
visibility = "public"
domain {
// this is the domain to query your agent update the ID with your org ID from liquidmetal.ai
fqdn = "agent.<YOUR ORG ID>.lmapp.run"
}
}
}
Once you have created this manifest, deployment is straightforward.
First, run raindrop build generate
to create all the necessary resources
based on your manifest. Next, add your TypeScript implementation to the project
directory. Finally, execute raindrop build deploy
to deploy your application to
the platform.
Note that the code we’ve explored in previous sections - the agent loop and tools implementation - is part of a larger application structure. The complete project includes additional components for handling service requests and a main agent class that ties everything together. You’ll find all these components in our source code repository, which we’ll link to in the next section.
You can download the entire source code for this project from our GitHub repository. To deploy it in its current form, you’ll need a LiquidMetal account, which you can create at liquidmetal.ai/build. By using our platform, we handle all the infrastructure setup and ensure your project has access to all required resources.
If you prefer to deploy independently, you can set up a TypeScript-compatible environment on your preferred cloud provider. Note that this approach may require modifications to the code, particularly around secrets management and resource availability.
[1] ReAct: Synergizing Reasoning and Acting in Language Models, Shunyu Yao and Jeffrey Zhao and Dian Yu and Nan Du and Izhak Shafran and Karthik Narasimhan and Yuan Cao, 2023, https://arxiv.org/abs/2210.03629