Giving an LLM Its First Tool: How to Let an AI Agent Read Your Project Files
🚀 Welcome to the second installment of the No Framework, Handcrafted AI Agent column: giving the agent the ability to read files.
Even if you haven't read the previous one, you can absolutely start right here.
The previous article really only did one basic thing: set up the project skeleton so we could ask the large language model questions in the terminal and get a "one question, one answer" flow working.
In this article, we're going to start giving this AI, which currently only has a "brain," some "hands and feet" so it can read files in our project.
Because while the previous version could answer questions, it had a major flaw: it couldn't see your project at all.
First, let's "set the hook" by looking at the "complete form" we'll eventually build by the end of this series
In the previous version of the Agent, if you typed:
npm run start -- -prompt "What does this project do?"
It could only combine your question with its own existing knowledge to guess.
It didn't know what files were in your project, nor what was actually written in README.md.
The reason is simple: our current Agent only has the large language model as a "brain"; we haven't given it any tools yet.
The large language model is very smart. It can understand your questions, analyze your needs, and even produce a seemingly reliable plan. But it has no "hands" or "feet"; it can't open the files in your project by itself, much less read their contents.
In real development, we want the Agent not just to "be able to answer," but also to help us view, read, and modify files in the project.
You: What does this project do?
AI: Let me first look at the README.
AI: I've read it. This project is...
So in this article, we'll do exactly that: give this smart brain its first pair of hands.
Let's start with the most fundamental capability: reading files.
First, prepare a README for the AI to read
In this article, we want the AI to read README.md, so first create this file in the project root:
powercode/
├── README.md
├── package.json
└── src/
Write the following into README.md:
# powercode
This is a TypeScript teaching project for implementing an AI Agent from scratch.
Currently, it can already answer questions from the large language model in the terminal.
Next, we will add capabilities like reading files, modifying files, and executing commands.
Later, we'll have the AI read this README to see if it can understand what the project does.
First, look at the effect we'll achieve in this section
Once the code is written, you can ask:
npm run start -- -prompt "Help me read README.md; what does this project do?"
The running effect is as follows:
The key point isn't the final answer, but this line in the middle:
AI wants to read file: README.md
This means the agent didn't just make things up; it genuinely went to look at the file in the project first.
🚀 Source code for this section: powercode 👈Click it
If you run into problems midway, you can check the source code for troubleshooting. The code for subsequent chapters will also be continuously updated. If this project helps you, a Star ⭐ is welcome.
First, understand: How does the AI "read a file"?
As we said earlier, the AI cannot directly open files on your computer.
What it can do is tell us:
"I want to read the project's README.md file."
Then, we write a program to read the files in our project.
You can think of it like this:
AI: Help me read README.md
↓
Our program: Okay, I'll go read it
↓
Our program: Here is the README content
↓
AI: Okay, then I'll answer the user based on that content
This is what's called "tool calling."
The name sounds fancy, but it's really just:
The AI makes a request, and the program does the work.
In this article, we'll only give it one tool:
read_file: Read a file
Get one tool working well first; adding file writing, file modification, and command execution later will be simple.
Step Two: Create the read_file tool
In the project from the previous chapter, create a new file:
src/readFile.ts
Copy the following code into it; we'll explain it piece by piece below:
import { readFile } from 'node:fs/promises';
import { resolve, sep } from 'node:path';
const MAX_BYTES = 8_000; // Maximum bytes
/**
* Read file tool configuration
*/
export const READ_FILE_TOOL = {
type: 'function',
function: {
name: 'read_file',
description: 'Read the content of a text file in the current project.',
parameters: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'File path, e.g., README.md',
},
},
required: ['path'],
additionalProperties: false,
},
},
} as const;
/**
* Get file path
* @param argumentsJson Parameter JSON
* @returns File path
*/
function getPath(argumentsJson: string): string {
const input = JSON.parse(argumentsJson) as { path?: unknown };
if (typeof input.path !== 'string' || input.path.trim() === '') {
throw new Error('read_file requires a file path.');
}
return input.path;
}
/**
* Read file
* @param argumentsJson Parameter JSON
* @returns File content
*/
export async function readFileTool(argumentsJson: string): Promise<string> {
const relativePath = getPath(argumentsJson);
const workDir = resolve(process.cwd());
const targetPath = resolve(workDir, relativePath);
const isOutsideWorkDir =
targetPath !== workDir && !targetPath.startsWith(`${workDir}${sep}`);
if (isOutsideWorkDir) {
throw new Error('Can only read files within the current project.');
}
const content = await readFile(targetPath);
if (content.length > MAX_BYTES) {
return `${content.subarray(0, MAX_BYTES).toString('utf8')}
...[File too long, only returning the first ${MAX_BYTES} bytes]...`;
}
return content.toString('utf8');
}
This code looks long, but it really does just two things.
First thing: Tell the AI it now has a new skill
This part of the code is for the AI to see; it's not what actually reads the file.
You can think of it as a "tool manual":
Tool name: read_file
What it does: Reads file content
What to pass when using it: File path `path`
After the AI reads this manual, it knows: if it wants to read a file, it can request to call read_file and tell us the file path at the same time.
The formal name for this "tool manual" is JSON Schema.
Simply put, JSON Schema is: using a piece of JSON-formatted content to clearly explain "what this tool is called, what it can do, and what parameters it needs."
For example, the following snippet:
function: {
name: 'read_file',
description: 'Read the content of a text file in the current project.',
parameters: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'File path, e.g., README.md',
},
},
required: ['path'],
},
}
Is telling the AI:
There is a tool here called
read_filethat can read text files.
When calling it, you must pass a string parameter calledpath.
So, when the AI thinks "I need to read the README first to answer," it will issue a request like this:
{
"path": "README.md"
}
Second thing: Actually read the file
The "tool manual" above just tells the AI: it can request to read a file.
The line that actually does the work is this one:
const content = await readFile(targetPath);
readFile is a built-in file reading method in Node.js.
The AI is only responsible for making the request:
I want to read README.md.
And our program, based on the file path sent by the AI model, calls Node.js to read the real file, and finally hands the read result back to the AI.
So you can simply understand it as:
The AI is responsible for saying "which file I want to read"
↓
Node.js is responsible for actually reading the file
Node.js is the one that truly reads out the file content.
Why can't we let the AI read files arbitrarily?
Just now, we mentioned using readFile to read file content, but we must limit the scope of files the AI can read. For example, when we usually use an agent, we hope the agent can only read files in the directory where the agent was started, and cannot access anything else. So we added this restriction in the code:
if (isOutsideWorkDir) {
throw new Error('Can only read files within the current project.');
}
The meaning is simple:
Files within the current project: Can be read
Files outside the project: Cannot be read
Additionally, we also limited the file size:
const MAX_BYTES = 8_000;
The reason is also straightforward: some files are very large.
For example, log files, bundled code, or files with hundreds of thousands of lines of data. Stuffing all of that into the AI is not only slow but also costs extra tokens.
We'll limit it to 8,000 bytes first; everyone can adjust this according to their own needs.
Hand the tool over to the AI
Open src/chat.ts from the previous chapter and replace it with the following complete code:
import OpenAI from 'openai';
import type { ProviderConfig } from './config.js';
import { READ_FILE_TOOL } from './readFile.js';
export class ChatClient {
private readonly client: OpenAI;
constructor(private readonly config: ProviderConfig) {
this.client = new OpenAI({
apiKey: config.apiKey,
baseURL: config.baseURL,
});
}
async askWithTools(prompt: string) {
const response = await this.client.chat.completions.create({
model: this.config.model,
messages: [
{
role: 'system',
content:
'You are power-code, a development assistant. When you need to understand project content, prioritize reading real files. Please answer in Chinese.',
},
{
role: 'user',
content: prompt,
},
],
tools: [READ_FILE_TOOL],
});
return response.choices[0]?.message;
}
async answerAfterReadFile(
prompt: string,
toolCall: any,
fileContent: string,
): Promise<string> {
const response = await this.client.chat.completions.create({
model: this.config.model,
messages: [
{
role: 'system',
content:
'You are power-code, a development assistant. Please answer the user in Chinese based on the real file content returned by the tool.',
},
{
role: 'user',
content: prompt,
},
{
role: 'assistant',
content: null,
tool_calls: [toolCall],
},
{
role: 'tool',
tool_call_id: toolCall.id,
content: fileContent,
},
],
});
return response.choices[0]?.message.content ?? 'The model did not return any content.';
}
}
The most critical line here is this one:
tools: [READ_FILE_TOOL],
In the previous article, we only told the AI model:
What the user asked
In this article, we told it one more thing:
You now have the read_file tool. If you want to read a file, you can request to call it.
Note, it's just "can request."
The AI cannot read directly; it must first ask our program.
Catch the AI's request
Open src/main.ts again and replace it with the following complete code:
import { ChatClient } from './chat.js';
import { loadConfig } from './config.js';
import { readFileTool } from './readFile.js';
/**
* Get the user's question from command line arguments
* @param args Command line arguments
* @returns The user's question
*/
function getPrompt(args: string[]): string {
const promptIndex = args.indexOf('-prompt');
if (promptIndex === -1) {
throw new Error('Please pass a question via -prompt, e.g., -prompt "Hello"');
}
const prompt = args[promptIndex + 1];
if (!prompt) {
throw new Error('The content after -prompt cannot be empty.');
}
return prompt;
}
/**
* Main function, responsible for handling command line arguments, loading config, interacting with the model, and printing results.
* @returns
*/
async function main() {
// Get the user's question from command line arguments
const prompt = getPrompt(process.argv.slice(2));
// Load configuration
const config = await loadConfig();
// Create a ChatClient instance
const client = new ChatClient(config);
console.log('AI is thinking...\n');
// Call the model, get the first message
const firstMessage = await client.askWithTools(prompt);
// Get the tool call
const toolCall = firstMessage?.tool_calls?.[0];
if (!toolCall) {
console.log(`AI:${firstMessage?.content ?? 'The model did not return any content.'}`);
return;
}
if (toolCall.type !== 'function') {
throw new Error(`Tool type not currently supported: ${toolCall.type}`);
}
if (toolCall.function.name !== 'read_file') {
throw new Error(`Tool not currently supported: ${toolCall.function.name}`);
}
console.log(`AI wants to read file: ${toolCall.function.arguments}\n`);
console.log(`Reading file: ${toolCall.function.arguments}`);
const fileContent = await readFileTool(toolCall.function.arguments);
const answer = await client.answerAfterReadFile(
prompt,
toolCall,
fileContent,
);
console.log(`AI:${answer}`);
}
main().catch((error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
console.error(`Startup failed: ${message}`);
process.exit(1);
});
Don't be intimidated by the length of the code; the main flow is really just three steps:
First, ask the AI:
The user's question is here. Do you want to answer directly, or use a tool?
const firstMessage = await client.askWithTools(prompt);
If the AI doesn't need to read a file, output the answer directly:
if (!toolCall) {
console.log(`AI:${firstMessage?.content ?? 'The model did not return any content.'}`);
return;
}
If it wants to read the README, let our program read it for it:
const fileContent = await readFileTool(toolCall.function.arguments);
Finally, hand the real content of the README back to the AI:
const answer = await client.answerAfterReadFile(
prompt,
toolCall,
fileContent,
);
Don't miss this step.
Because the first time, the AI only said: "I want to read the README."
After reading it, we must send the content back so it can say: "I've read it; this project does..."
Let's run it and try
First, compile:
npm run build
Then run:
npm run start -- -prompt "Help me read README.md; what does this project do?"
Under normal circumstances, you will see output similar to:
AI is thinking...
AI wants to read file: {"path":"README.md"}
AI: This project is a TypeScript teaching project for implementing an AI Agent from scratch...
What exactly did we do in this article?
To sum it up in one sentence:
Before, the AI could only guess; now, when it encounters something it doesn't know, it will first request to read a file.
The complete flow is:
User asks a question
↓
AI discovers it needs to read the README
↓
AI requests to call read_file
↓
Our Node.js program reads the README
↓
Send the README content back to the AI
↓
AI answers based on the real content
At this point, it finally has a bit of the look of an AI Agent.
A small reminder
Currently, it can only read once
We deliberately only let it read a file once in this article.
Because the focus isn't on piling on features, but on thoroughly understanding this one thing:
AI requests a tool
→ Program executes the tool
→ Tool result is handed back to the AI
In the next article, we'll tackle a more interesting problem:
What if, after reading the README, the AI wants to read
package.jsontoo?
At that point, we'll let it enter a true Agent loop:
Think
→ Use a tool
→ See the result
→ Think again
Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.
Big shot!!
Just call me little bro [heart hands]