跪拜 Guibai
← Back to the summary

How an AI Agent Actually Reads a File (It's Not the Model)

The closed loop of model, Harness, and file system for reading files

When you say to an Agent: "Check README.md for me, what is this project about?"

It seems like just a sentence, but behind it, it's not "the model opened the file itself." Large models run on the server side; they can neither see your disk nor naturally have file permissions. What really happens is: the model proposes a tool request, the program executes it locally, and then returns the result as new context to the model.

This article won't rush into piling up code. We'll first follow a real read_file call and thoroughly clarify the following questions:

First, remember the most important sentence in the whole text:

What limits the model is not "intelligence," but that the current program has not yet connected it to tools, permissions, and a result feedback mechanism.

01|The model can see the context, but not your disk

A boundary exists between the model's context and the local disk

Each time the model performs inference, what it can truly see is only the context within that request, for example:

The README.md, package.json, and source code directories on your computer do not automatically enter the model's context just because the user mentioned them.

So the model can understand the sentence "please read the README," but it doesn't know what is actually written inside the README. Without tools, it can only guess based on existing knowledge; no matter how accurate the guess, it is not the same as reading the real file.

This is also the first line for judging whether an Agent has "touched the real world": Do the facts in the answer come from the model's memory, or from a verifiable external read?

02|A single file read actually involves five participants

The division of labor among User, Model, Harness, Tool, and File System

A single read_file seems simple, but it actually involves at least five participants:

  1. User: Proposes a goal, such as "How do I start this project?"
  2. Model: Judges whether it can answer based solely on the existing context, and which file is needed next.
  3. Harness: Organizes the model request, provides tools, validates calls, executes functions, and returns results.
  4. Tool Function: Turns structured parameters into a real operation, such as calling a file reading API.
  5. File System: Stores the real README, code, and configuration; it is the source of truth.

Their complete relationship is:

User asks → Harness requests model → Model returns Tool Call → Harness validates and executes → File system returns content → Harness returns Tool Result → Model generates answer

The most confusing point here is: The model is responsible for deciding, and the Harness is responsible for executing. The model does not directly execute local code, nor does it bypass the program to gain file permissions.

03|A tool is actually composed of two halves: a "manual" and an "executor"

Tool Schema and the tool execution function are the two halves of a tool

To enable the model to use read_file, the program needs to prepare two things:

These two parts cannot replace each other.

With only a Schema and no executor, the model will propose a "read README" request, but the program cannot complete the action; with only an executor and no Schema, the program has the reading capability, but the model doesn't know when or in what format to apply for its use.

Many frameworks wrap this step as "registering a tool." But in the corresponding minimal implementation for this article, there is no extra tool registry: we simply define READ_FILE_TOOL, place it in the tools array of the model request, and upon receiving a call, main.ts explicitly dispatches it to readFileTool.

04|Tool Schema is a model-readable function contract

The four key fields of the read_file Tool Schema

The core description of read_file can be simplified to:

{
  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' }
      },
      required: ['path'],
      additionalProperties: false
    }
  }
}

This contract answers at least four questions:

additionalProperties: false also expresses a stricter constraint: do not arbitrarily attach undeclared fields.

But note: Schema is only a calling format, not a security boundary. Even if the Schema requires path to be a string, the model could still send an empty string, an out-of-bounds path, or a non-existent file. Real security validation must be completed by the Harness and the tool executor.

05|The Harness gives the tool description to the model, not the file to the model

The model request carries both messages and tool descriptions

After the user asks a question, the Harness initiates the first model request:

const response = await client.chat.completions.create({
  model,
  messages: [systemMessage, userMessage],
  tools: [READ_FILE_TOOL]
})

At this point, the model receives two types of information:

  1. messages: What problem the user wants to solve;
  2. tools: If information is missing, what capabilities can be requested.

The model still hasn't seen the body of README.md. It only knows that a tool named read_file exists and that it needs to provide a path when calling it.

This step is much like giving the model a menu: the menu tells it "what can be ordered," but the dishes haven't been served yet.

06|A Tool Call is a structured action request, not an execution result

The model returns a read_file Tool Call and parameters

If the model determines it must read the README first, it won't fabricate the file content directly but will return a Tool Call. Conceptually, it's similar to:

{
  "id": "call_123",
  "type": "function",
  "function": {
    "name": "read_file",
    "arguments": "{\"path\":\"README.md\"}"
  }
}

Three details are important here:

So a Tool Call is more like an "application form": the model expresses its next intended step, but the disk hasn't been read yet, and the execution right remains in the Harness's hands.

07|The Harness must first identify, validate, and then decide whether to execute

The Harness performs three layers of validation on tool type, name, and parameters

After receiving a Tool Call, the Harness should not directly execute the content given by the model as a trusted command. The minimal implementation performs three layers of checks in sequence:

if (toolCall.type !== 'function') { /* reject */ }
if (toolCall.function.name !== 'read_file') { /* reject */ }

const input = JSON.parse(toolCall.function.arguments)
if (typeof input.path !== 'string' || input.path.trim() === '') {
  throw new Error('read_file requires a file path.')
}

They are respectively confirming:

  1. Is this a tool call type we support?
  2. Is this a tool we allow to execute?
  3. Can the parameters be parsed, and is path really a non-empty string?

This reflects a very important principle in Agent engineering:

The model can propose actions, but the program must retain the final execution right.

When connecting high-risk tools like writing files, executing commands, or accessing networks in the future, this validation layer will need to add further rules for permissions, approval, timeouts, and auditing.

08|Path validation determines where the tool can read

Workspace path normalization and out-of-bounds interception

Just validating that path is a string is not enough. Suppose the model sends:

../../secret.txt

If passed directly to the file API, it could escape the current project directory. The implementation code first converts both the working directory and the target path into absolute paths:

const workDir = resolve(process.cwd())
const targetPath = resolve(workDir, relativePath)

const isOutsideWorkDir =
  targetPath !== workDir &&
  !targetPath.startsWith(`${workDir}${sep}`)

After normalization via resolve, ./src/../README.md is restored to its real target, and ../../secret.txt also exposes that it has run outside the workspace. Only if the target equals the workspace, or is located under "workspace path + system separator," is it allowed to continue.

Why add ${sep}? Because judging only by string prefix can cause false positives: /project-evil starts with /project but is not a subdirectory of /project.

This boundary illustrates: Tool capability does not equal unlimited permission. read_file can read files, but it should only read the scope explicitly allowed by the current task.

09|What actually reads the file is the local executor

The local executor reads the file and performs size truncation

After passing validation, the Harness calls the local tool function:

const content = await readFile(targetPath)

if (content.length > 8_000) {
  return content.subarray(0, 8_000).toString('utf8') +
    '\n\n...[File too long, only returning the first 8000 bytes]...'
}

return content.toString('utf8')

What actually touches the disk here is the Node.js file API, not the model.

The code also limits a single read to 8,000 bytes. The reason is not that "the model can't read long files," but that tool output enters the model's context: the larger the file, the slower the request, the more tokens consumed, and the easier it is to crowd out truly important information.

This minimal implementation treats files as UTF-8 text; in a production environment, one usually needs to further consider binary files, encoding, timeouts, sensitive information, large log files, and reading by line or range.

If the file does not exist or the system denies access, the underlying read will throw an error; if the file is too large, this implementation does not fail but returns the first 8,000 bytes and explicitly marks it as "truncated."

10|The Tool Result must match the original Tool Call

Tool Call and Tool Result correspond via tool_call_id

After the file read is complete, the content hasn't automatically returned to the model. The Harness needs to initiate a second model request and bring back the complete conversation relationship:

messages: [
  { role: 'user', content: prompt },
  {
    role: 'assistant',
    content: null,
    tool_calls: [toolCall]
  },
  {
    role: 'tool',
    tool_call_id: toolCall.id,
    content: fileContent
  }
]

This is not simply sending another piece of "README content." assistant.tool_calls records what request the model just made, and tool.tool_call_id indicates which call this result is answering.

If multiple tool calls exist simultaneously, this ID becomes even more important: it prevents the model from mismatching the result of tool A to tool B.

Therefore, the essence of a Tool Result is: converting the execution result from the external world into context that the model can understand in the next round.

11|Why can it read files now, but it's not yet a complete Agent Loop?

The difference between a single tool call and a continuous Agent Loop

Connecting the entire chain, a single read actually goes through two model requests:

  1. First request: The model determines if a tool is needed and returns a Tool Call.
  2. Local execution: The Harness validates the path, reads the file, and gets the Tool Result.
  3. Second request: The model receives the real content and then organizes the final answer.

This already forms the smallest "Decision → Action → Feedback" closed loop, but the corresponding implementation deliberately handles only one tool call:

So, being able to call a tool once does not equal having an Agent Loop. A complete loop also requires repeatedly checking the model's return: if there is a Tool Call, continue executing and returning it; if there is no Tool Call, then end.

Corresponding to the implementation code

If you continue reading the accompanying implementation, you can use the following map to locate things:

Concept Implementation Location Responsibility
Tool Schema READ_FILE_TOOL in src/readFile.ts Tells the model the tool name, description, and parameters
Parameter & Path Validation getPath, readFileTool Rejects empty paths and paths outside the workspace
Tool Provision tools: [READ_FILE_TOOL] in src/chat.ts Places the tool description into the first model request
Tool Call Dispatch src/main.ts Identifies the tool and calls the executor
Tool Result Return answerAfterReadFile Returns the file content to the model using role: 'tool'

Accompanying source code: powercode / demos / 02-read-file

Finally, let's wrap it up with one sentence:

The model does not directly read the file. It selects the tool and generates the request; the Harness controls permissions, executes the action, and then feeds the real result back to the model.

In the next article, we will continue to expand "one tool call" into a true Agent Loop, seeing how it continuously reads multiple files and completes a task step by step.