In the previous chapter, we established a mental model for AI agents and understood why a specialized framework like Flue is essential for their reliable deployment. Now, it’s time to transition from theory to practice and construct our first functional agent. This chapter will walk you through the core architecture of a Flue agent, demonstrate how to integrate simple tools, and guide you in structuring your agent using TypeScript.
Our goal is not just to build a working agent, but to truly understand why Flue’s design principles—like the agent harness and state management—are critical for building robust, production-ready AI systems. You’ll gain hands-on experience by creating a simple agent that can respond to inputs and interact with a custom tool, setting the stage for more complex agent development.
To make the most of this chapter, please ensure you have Node.js (version 20.x or later, as of June 2026) and TypeScript installed. Familiarity with basic LLM coding paradigms (like those found in Claude Code, Codex, or OpenAI’s APIs) will also be beneficial.
The Agent Harness: Beyond Basic LLM Wrappers
When you hear “AI agent,” your first thought might be a simple function that calls an LLM API. While that’s a component, Flue introduces the concept of an agent harness – a comprehensive runtime environment that elevates agents far beyond mere API wrappers.
📌 Key Idea: A Flue agent is more than just an LLM. It’s an intelligent process capable of maintaining state, executing code in a sandboxed environment, and interacting with its surroundings through well-defined tools. The harness provides this structured, controlled execution context.
Why Does the Agent Harness Matter?
Developing sophisticated AI agents directly with LLM SDKs often leaves developers grappling with critical infrastructure challenges:
- State Management: How does an agent remember previous interactions? Without a harness, you’re left to implement complex, error-prone session management yourself.
- Secure Execution: If an agent needs to run code or access external systems, how do you ensure it operates safely and within defined boundaries? The harness provides the foundation for sandboxed execution.
- Tool Orchestration: How does the LLM intelligently select and use external functions or data sources? The harness provides a standardized mechanism for tool definition and invocation.
- Deployment and Scalability: How do you deploy and manage multiple, potentially long-running, stateful agents reliably? The harness abstracts away much of this complexity.
Flue’s agent harness addresses these challenges head-on by providing a structured, opinionated environment that facilitates secure, stateful, and tool-augmented agent behavior.
This diagram illustrates the core components within the Flue Agent Harness and how they interact. The Agent Route Handler acts as the entry point, directing user requests to the LLM, which then leverages the Agent State Manager, Sandboxed Execution Environment, and Tool Registry to process requests and interact with external systems.
Agents as Stateful Entities
Consider an AI assistant that remembers your name, preferences, or the context of a multi-step task. This capability hinges on statefulness. Flue agents are inherently designed to maintain state across multiple interactions, enabling:
- Multi-turn Conversations: The agent seamlessly retains conversational context, making interactions feel natural and continuous.
- Complex Workflows: An agent can track its progress through intricate processes, such as code generation or data analysis, remembering intermediate results.
- Personalization: Agents can remember user-specific data or historical interactions, tailoring responses and actions over time.
This state is securely managed by the Flue harness, abstracting away the complexities of persistence and retrieval, allowing you to focus on agent logic.
Tools and Skills: Extending Agent Capabilities
An agent that can only generate text is limited. Real-world AI agents must act. Flue facilitates this through Tools (sometimes referred to as skills), which are specific, well-defined functions or capabilities that you expose to your agent.
⚡ Real-world insight: Think of tools as the agent’s “API clients” or “execution environment.” They provide the agent with the means to interact with the external world—fetching data, calling APIs, sending emails, or even executing code in a controlled, sandboxed manner.
Examples of tools that Flue agents can integrate include:
MarkdownGenerator: For formatting output consistently.FileEditor: To read from or write to a sandboxed filesystem, crucial for coding agents.Calculator: To perform precise mathematical computations.APICaller: To make HTTP requests to integrate with any external service.
Each tool is defined with a clear name, description, and an inputSchema (using JSON Schema), which instructs the LLM on how and when to use the tool, including the arguments it expects. This clear interface is vital for both agent reliability and security.
Structuring Agents in TypeScript
Flue embraces a TypeScript-first approach. This strong typing and structured environment bring significant benefits to agent development:
- Type Safety: Catch errors during compilation, not at runtime, leading to more stable agents.
- Enhanced Readability: Clear interfaces and types make code easier to understand, maintain, and onboard new team members.
- Improved Developer Experience: Benefit from IDE autocompletion, robust refactoring tools, and clear documentation generated from type definitions.
You will define your agent’s behavior, its available tools, and how it processes inputs and generates outputs using TypeScript classes and interfaces, ensuring a consistent and robust development workflow.
Step-by-Step Implementation: Building a Simple Echo Agent with a Tool
Let’s put these concepts into practice by building a basic Flue agent. This agent will echo messages and demonstrate the integration of a simple “logger” tool.
1. Project Setup
If you’re continuing from a previous setup, ensure your my-first-flue-agent directory is ready. Otherwise, let’s create a new Node.js project and install the necessary Flue packages, along with TypeScript and Express:
mkdir my-first-flue-agent
cd my-first-flue-agent
npm init -y
npm install @flue/core @flue/express-handler @flue/core-llms-openai typescript @types/node express @types/express dotenv
npx tsc --init # Initialize TypeScript configurationNext, let’s refine our tsconfig.json for a modern Node.js environment. Open tsconfig.json and update it to match the following:
// tsconfig.json
{
"compilerOptions": {
"target": "es2022", // Target modern ECMAScript features
"module": "commonjs", // Use CommonJS for Node.js modules
"rootDir": "./src", // Source files are in the 'src' directory
"outDir": "./dist", // Compiled JavaScript goes into 'dist'
"esModuleInterop": true, // Allow default imports from modules with no default export
"forceConsistentCasingInFileNames": true, // Enforce consistent casing in file names
"strict": true, // Enable all strict type-checking options
"skipLibCheck": true, // Skip type checking of declaration files
"lib": ["es2022"], // Include ES2022 standard library features
"moduleResolution": "node" // Node.js-style module resolution
},
"include": ["src/**/*.ts"], // Include all .ts files in src
"exclude": ["node_modules"] // Exclude node_modules from compilation
}This configuration ensures our TypeScript code compiles correctly for Node.js and benefits from strict type checking.
Finally, create a src directory where all our agent code will reside:
mkdir src2. Defining a Simple Tool: The Logger
Our first tool will be a Logger. This tool will simply print a message to the console, simulating an agent taking an action that has an observable side effect.
Create the file src/tools/Logger.ts:
// src/tools/Logger.ts
import { Tool } from '@flue/core';
/**
* A simple Logger tool for agents to output messages to the console.
* This simulates an agent taking an action that has a side effect (logging an event).
*/
export class Logger extends Tool {
// Every tool needs a unique name. The LLM uses this to identify the tool.
public readonly name = 'Logger';
// A clear description helps the LLM understand when and why to use this tool.
public readonly description = 'Logs a message to the console for debugging or event tracking.';
/**
* The inputSchema defines the structure of arguments this tool expects.
* This is crucial for the LLM to know how to call the tool correctly.
* We use 'as const' to ensure TypeScript infers the most specific type.
*/
public readonly inputSchema = {
type: 'object',
properties: {
message: {
type: 'string',
description: 'The message content to log.',
},
},
required: ['message'], // The 'message' property is mandatory for this tool.
} as const;
/**
* The execute method contains the core logic of the tool.
* It receives validated input from the LLM and performs its action.
* @param input The validated input object, matching inputSchema.
* @returns A promise resolving to a string indicating the result of the tool's action.
*/
public async execute(input: { message: string }): Promise<string> {
// Perform the side effect: logging the message to the server console.
console.log(`[AGENT LOG]: ${input.message}`);
// Return a message that the agent can use in its response to the user.
return `Message successfully logged: "${input.message}"`;
}
}Explanation of the Logger Tool:
import { Tool } from '@flue/core';: All tools in Flue extend the baseToolclass, providing a consistent interface.public readonly name = 'Logger';: This is how the LLM will refer to this tool. It must be unique within an agent.public readonly description = '...';: A well-written description helps the LLM understand the tool’s purpose and when to invoke it.public readonly inputSchema = { ... } as const;: This JSON Schema-like object is vital. It formally defines the arguments theexecutemethod expects. The LLM uses this schema to generate appropriate tool calls.as constensures TypeScript infers the most specific type for compile-time safety.public async execute(input: { message: string }): Promise<string> { ... }: This method contains the actual logic. When the LLM decides to use theLoggertool, it will provide aninputobject that conforms to theinputSchema. OurLoggersimply prints this message to the console and returns a confirmation string.
3. Creating Your First Flue Agent
Now, let’s create our EchoAgent. This agent will be able to simply echo back user messages or, if prompted correctly, use our new Logger tool.
Create the file src/agents/EchoAgent.ts:
// src/agents/EchoAgent.ts
import { Agent, AgentContext, AgentStep, LLM } from '@flue/core';
import { Logger } from '../tools/Logger'; // Import our custom Logger tool
/**
* An EchoAgent demonstrates basic agent functionality:
* - Responding to user input directly.
* - Simulating tool usage based on specific input patterns.
*/
export class EchoAgent extends Agent {
// A unique name for our agent.
public readonly name = 'EchoAgent';
// A description of what this agent does.
public readonly description = 'An agent that echoes messages and can log them using a Logger tool.';
/**
* The constructor is where we initialize the agent and register its available tools.
* @param llm The Language Model instance this agent will use for reasoning.
*/
constructor(llm: LLM) {
super(llm); // Call the base Agent class constructor with the LLM.
// Register our custom Logger tool, making it available for the agent to use.
this.registerTool(new Logger());
}
/**
* The core logic for how the agent processes an incoming input.
* In a real-world scenario, the LLM would decide on actions.
* Here, we use a simple 'if' condition to simulate tool invocation.
* @param context The current agent context, including user input and session state.
* @returns A promise resolving to an AgentStep, which defines the agent's next action or response.
*/
public async handle(context: AgentContext): Promise<AgentStep> {
const { input } = context; // Extract the user's input from the context.
// For this introductory agent, we'll use a simple heuristic to trigger the tool.
// In more advanced Flue agents, the LLM itself would analyze the input,
// consult its registered tools (using their names and descriptions),
// and decide if a tool call is appropriate.
if (input.startsWith('log:')) {
const messageToLog = input.substring(4).trim(); // Extract the message after "log:"
console.log(`EchoAgent received request to log: "${messageToLog}"`);
// Retrieve the registered Logger tool instance.
const loggerTool = this.getTool('Logger') as Logger;
if (loggerTool) {
// Execute the tool with the extracted message.
const toolOutput = await loggerTool.execute({ message: messageToLog });
// Return a response step, informing the user about the tool's action.
return {
type: 'response',
content: `I've used the Logger tool for you. Tool said: ${toolOutput}`,
};
} else {
// Fallback if the tool isn't found (shouldn't happen if registered correctly).
return {
type: 'response',
content: `Error: Logger tool was requested but not found.`,
};
}
} else {
// If the input doesn't trigger a tool, simply echo the user's message.
return {
type: 'response',
content: `You said: "${input}"`,
};
}
}
}Explanation of the EchoAgent:
import { Agent, AgentContext, AgentStep, LLM } from '@flue/core';: These are core Flue types.Agentis the base class for all agents.AgentContextholds the current interaction’s data, andAgentStepdefines what the agent does next.constructor(llm: LLM): Every Flue agent needs an LLM to power its reasoning. We pass anLLMinstance to the baseAgentconstructor.this.registerTool(new Logger());: This line is crucial! It makes ourLoggertool available to this specificEchoAgent. Without this, the agent wouldn’t know about the tool.public async handle(context: AgentContext): Promise<AgentStep>: This is the heart of your agent’s logic. It’s called whenever the agent receives a new input. Thecontextobject provides theinput(user’s message) and access to the agent’sstate.- Simulated Tool Use vs. LLM Reasoning: For simplicity in this introductory chapter, we’re using a direct
if (input.startsWith('log:'))condition to trigger ourLoggertool.- In a real, advanced Flue agent, the LLM itself would receive the user’s
input, analyze it, consult the descriptions and schemas of all registered tools, and then decide which tool (if any) to call, and with what arguments. This decision-making process is a key differentiator of the agent harness. Our current approach just helps us see the tool integration in action without complex LLM prompting for tool selection.
- In a real, advanced Flue agent, the LLM itself would receive the user’s
return { type: 'response', content: '...' };: An agent’shandlemethod must always return anAgentStep. For now, we’re returning aresponsestep, which means the agent is outputting text back to the user. Other step types includetoolCall(to instruct the LLM to call a tool) orerror.
4. Setting up the Express Server with AgentRouteHandler
To make our EchoAgent accessible via HTTP, we’ll use Express.js and Flue’s AgentRouteHandler, which simplifies exposing agents as API endpoints.
Create the file src/index.ts:
// src/index.ts
import express from 'express';
import { AgentRouteHandler } from '@flue/express-handler'; // Flue's Express integration
import { OpenAI } from '@flue/core-llms-openai'; // Example LLM provider (OpenAI)
import { EchoAgent } from './agents/EchoAgent'; // Our custom agent
import dotenv from 'dotenv'; // For loading environment variables
dotenv.config(); // Load environment variables from the .env file
const app = express();
app.use(express.json()); // Enable Express to parse JSON request bodies
const port = process.env.PORT || 3000; // Define the port for our server
// 🧠 Important: An LLM API key is essential for your agent to function.
// Flue supports various LLM providers (OpenAI, Claude, etc.).
// For this example, we're using OpenAI. Ensure OPENAI_API_KEY is set
// in your .env file or environment variables.
if (!process.env.OPENAI_API_KEY) {
console.error("Error: OPENAI_API_KEY environment variable is not set. Please add it to your .env file.");
process.exit(1); // Exit if the API key is missing.
}
// Initialize our chosen LLM. Flue's modular design allows you to swap LLMs easily.
// As of June 2026, ensure your LLM provider and API key are compatible.
const llm = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// Create an instance of our EchoAgent, passing the initialized LLM.
const echoAgent = new EchoAgent(llm);
// Create an AgentRouteHandler for our agent. This adapts the agent
// to work seamlessly with Express routes.
const agentHandler = new AgentRouteHandler(echoAgent);
// Define an HTTP POST endpoint for our agent.
// When a POST request hits '/agent/echo', the agentHandler will process it.
app.post('/agent/echo', agentHandler.handle);
// Add a basic health check endpoint for monitoring purposes.
app.get('/health', (req, res) => {
res.status(200).send('Agent server is running and healthy!');
});
// Start the Express server.
app.listen(port, () => {
console.log(`EchoAgent server listening at http://localhost:${port}`);
console.log(`\nTo test, open another terminal and use curl:`);
console.log(` - Simple echo: curl -X POST -H "Content-Type: application/json" -d '{"input": "Hello Flue!"}' http://localhost:${port}/agent/echo`);
console.log(` - Use Logger tool: curl -X POST -H "Content-Type: application/json" -d '{"input": "log: This is a test message for the agent."}' http://localhost:${port}/agent/echo`);
});Explanation of the Express Server:
import { AgentRouteHandler } from '@flue/express-handler';: This powerful helper class from Flue simplifies integrating your agent with Express. It handles the parsing of incoming requests, forwarding them to your agent, and formatting the agent’s response back to the client.import { OpenAI } from '@flue/core-llms-openai';: We’re importing theOpenAILLM provider. Flue is designed to be LLM-agnostic, so you can swap this with other providers likeClaudeor custom LLMs as needed.dotenv.config();: This line loads environment variables from a.envfile in your project root, which is the standard way to manage sensitive information like API keys securely.const llm = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });: Here, we instantiate our LLM. It’s critical that yourOPENAI_API_KEYenvironment variable is set.const agentHandler = new AgentRouteHandler(echoAgent);: We create an instance of theAgentRouteHandler, passing ourechoAgentto it. This handler now knows how to interact with our agent.app.post('/agent/echo', agentHandler.handle);: This line registers a POST route. Any incoming POST request to/agent/echowill be processed byagentHandler.handle, which in turn invokes ourEchoAgent.
5. Running Your Agent
Before running, we need to provide our OpenAI API key.
Create a new file named .env in the root of your my-first-flue-agent project (next to package.json):
# .env
OPENAI_API_KEY=your_openai_api_key_hereReplace your_openai_api_key_here with your actual OpenAI API key. Never commit your .env file to version control.
Now, compile your TypeScript code and run the server:
npx tsc # Compiles TypeScript files from src/ to dist/
node dist/index.js # Runs the compiled JavaScript serverYou should see output indicating your server is running and providing curl commands for testing:
EchoAgent server listening at http://localhost:3000
To test, open another terminal and use curl:
- Simple echo: curl -X POST -H "Content-Type: application/json" -d '{"input": "Hello Flue!"}' http://localhost:3000/agent/echo
- Use Logger tool: curl -X POST -H "Content-Type: application/json" -d '{"input": "log: This is a test message for the agent."}' http://localhost:3000/agent/echoOpen a new terminal window (keep the server running in the first one) and try the curl commands:
Test 1: Simple Echo Response
curl -X POST -H "Content-Type: application/json" -d '{"input": "Hello Flue!"}' http://localhost:3000/agent/echoExpected output in your curl terminal:
{"output":"You said: \"Hello Flue!\""}Test 2: Using the Logger Tool
curl -X POST -H "Content-Type: application/json" -d '{"input": "log: This is a test message for the agent."}' http://localhost:3000/agent/echoExpected output in your server terminal:
EchoAgent received request to log: "This is a test message for the agent."
[AGENT LOG]: This is a test message for the agent.And in your curl terminal:
{"output":"I've used the Logger tool for you. Tool said: Message successfully logged: \"This is a test message for the agent.\""}Congratulations! You’ve successfully built, configured, and run your first Flue agent with integrated tools. You’ve witnessed how Flue orchestrates agent responses and tool interactions.
Mini-Challenge: Extend Your Agent with a Greeter Tool
Now that you’ve built a basic agent and integrated a Logger tool, it’s your turn to expand its capabilities.
Challenge:
- Create a New Tool: In
src/tools/, create a new TypeScript file namedGreeter.ts. - Define
Greeter:- This tool should extend
@flue/core.Tool. - Give it a
name(e.g.,'Greeter') and adescription(e.g.,'Greets a person by name.'). - Its
inputSchemashould define a singlenameproperty of typestring, which isrequired. - The
executemethod should accept aninputobject with anameand return a friendly greeting string, such as"Hello, [name]! Nice to meet you.".
- This tool should extend
- Register with
EchoAgent: Modifysrc/agents/EchoAgent.tsto register an instance of your newGreetertool in its constructor. - Modify Agent Logic: Update
EchoAgent’shandlemethod. If theinputstarts with"greet:", extract the name (e.g.,greet: Aliceshould extractAlice), and use theGreetertool to generate a personalized greeting. - Test Your Agent: Restart your server and use
curlto test your agent’s new greeting capability. For example:curl -X POST -H "Content-Type: application/json" -d '{"input": "greet: Alice"}' http://localhost:3000/agent/echo
Hint: Follow the existing structure of the Logger tool and how EchoAgent’s handle method currently processes the "log:" prefix. Remember to import your new tool into EchoAgent.ts.
Common Pitfalls & Troubleshooting
Even with a simple agent, issues can arise. Here are some common problems and how to debug them:
- Missing API Key: This is the most frequent issue.
- Symptom: Server crashes on startup with an error like “OPENAI_API_KEY environment variable is not set.”
- Fix: Double-check your
.envfile. EnsureOPENAI_API_KEY(or the key for your chosen LLM) is correctly spelled, has a valid value, and is in the project root. Remember to restart your server after modifying.env.
- Tool Not Registered:
- Symptom: Your agent’s logic attempts to use a tool, but it’s never found (
this.getTool('ToolName')returnsundefined). - Fix: Verify that you’ve called
this.registerTool(new YourTool())in your agent’s constructor.
- Symptom: Your agent’s logic attempts to use a tool, but it’s never found (
- TypeScript Compilation Errors:
- Symptom:
npx tscfails with type errors or your IDE shows red squiggly lines. - Fix: Carefully review the error messages. Ensure your
tsconfig.jsonis correctly configured, especiallyrootDir,outDir, andmoduleResolution. Check for typos in type definitions.
- Symptom:
- Incorrect
inputSchema(for advanced LLM-driven tool use):- Symptom: In a more complex agent where the LLM decides to call tools, the LLM might fail to invoke your tool or provide incorrect arguments.
- Fix: While not fully demonstrated in this chapter (due to our simulated tool call), ensure your tool’s
inputSchemais precise and clearly describes the expected arguments. Ambiguous descriptions or missingrequiredfields can confuse the LLM.
- Port Conflicts:
- Symptom: Your server fails to start with an error like “listen EADDRINUSE: address already in use :::3000.”
- Fix: Another process is already using port 3000. You can change the
PORTenvironment variable in your.envfile (e.g.,PORT=3001) or identify and kill the conflicting process.
⚠️ What can go wrong: In real-world Flue agents, the LLM’s ability to correctly use tools hinges on clear, concise tool descriptions and accurate inputSchema definitions. If your agent struggles with tool use, first examine how you’ve presented the tool’s purpose and expected inputs.
Summary
In this chapter, you’ve taken a crucial step in understanding and building production-ready AI agents with the Flue Framework.
Here are the key takeaways:
- Agent Harness Architecture: You learned that Flue provides a robust “agent harness” that manages state, sandboxed execution capabilities, and tool orchestration, setting it apart from basic LLM API wrappers.
- Stateful Agents: Flue agents are designed to be inherently stateful, enabling them to maintain context across complex, multi-turn interactions.
- Tools and Skills Integration: You gained hands-on experience defining and integrating custom tools (like our
Logger), which empower your agents to perform actions and interact with the external world. - TypeScript-First Approach: Flue leverages TypeScript for strong typing, improved readability, and a more robust developer experience when structuring agents and their workflows.
AgentRouteHandlerfor Deployment: You saw howAgentRouteHandlersimplifies exposing your Flue agents as accessible HTTP endpoints, ready for integration into your applications.
You now have a foundational understanding of how to construct a Flue agent and integrate simple tools, laying the groundwork for more sophisticated AI applications. In the next chapter, we’ll delve deeper into advanced tool integration, explore more nuanced state management strategies, and begin to build truly intelligent and interactive coding agents.
References
- Flue — The Agent Harness Framework
- withastro/flue: The sandbox agent framework - GitHub
- Node.js Official Documentation
- TypeScript Official Documentation
- Express Official Documentation
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.