跪拜 Guibai
← Back to the summary

AI Agents Are Just a Brain and a Harness — Here's the Harness

Don't get confused by the term Agent; its core is actually just two parts.

The tools we use daily, such as Cursor, Trae, Claude Code, and Codex, are essentially AI Agents. Many people encountering Agent for the first time find it especially mysterious, as if it hides a very complex, unfathomable technology behind it. But if we "demystify" the AI Agent, it can actually be simplified into a very intuitive formula:

AI Agent = Large Language Model (Brain) + Harness (Reins)

The large model is responsible for thinking, and the Harness is responsible for ensuring it actually gets things done.

First, let's "set the bait" and see what kind of "complete form" we will eventually build by the end of this series

Kapture 2026-07-13 at 10.07.08.gif

As shown above, it will have terminal interaction similar to Claude Code, be able to perceive your current working directory, and autonomously decide how to help you complete development tasks.

Doesn't that feel cool?

But these capabilities are not innate to the model. The model itself is only responsible for understanding requirements and thinking about the next step; how to enter your working directory, call the terminal, read and write files, execute commands, and continue trying after failures relies on another operating mechanism.

This mechanism is the Harness.

What exactly is the Harness?

Harness literally translates to "horse gear" or "reins."

In AI engineering, this metaphor is actually very vivid.

Let's look at the simplest example.

When you say to Claude Code or Codex: Help me fix the login bug in this project.

The large model behind it cannot magically read your code, nor can it directly modify files.

What truly gives it these capabilities is the Harness providing it with:

So, having just a large model is not enough.

A large model without a Harness is more like a "brain" that can chat and give ideas; connected to a Harness, it can truly read code, modify files, execute commands, and become an AI Agent that can help us complete tasks.

The large model determines whether it can figure things out, and the Harness determines whether it can get things done well.

And what this series aims to do is to build such a Harness "reins" system with you from scratch.

Today's Goal: Get It Running First

Rome wasn't built in a day. Let's put aside the complex advanced capabilities for now; any powerful AI Agent has a very humble starting point.

In this first article of the series, we will personally type its first line of core code and get this most basic "one question, one answer" shortest link working:

image.png

The "minimum viable version" we are implementing today as the first step looks like this when running:

$ npm run start -- -prompt "Explain what TypeScript is in one sentence"

AI: TypeScript is a superset of JavaScript that adds static typing capabilities to JavaScript.

Note: It is still just a "chatty command-line assistant" right now, and cannot read files or modify code yet. But this is exactly the foundation for implementing an AI programming Agent later.


1. Prepare the Environment

First, check the Node.js version:

node -v

If the version is lower than v21, it is recommended to upgrade first.

image.png

2. Create a New Independent Project

Create a new folder powercode

mkdir powercode
cd powercode
npm init -y

Install dependencies:

npm install openai 
npm install -D typescript @types/node

Now the project directory roughly looks like:

powercode/
├── node_modules/
├── package.json
└── package-lock.json

3. Configure TypeScript

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*.ts"]
}

Then modify package.json to adjust it as follows:

{
  "name": "powercode",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/main.js"
  },
  "dependencies": {
    "openai": "^6.0.0"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "typescript": "^5.0.0"
  }
}

Key point: Note the addition of "type": "module" here, which is to allow us to use the modern import syntax directly in Node.js, rather than the old-style require.

4. Configure Your Large Model

Create provider.json in the project root directory:

{
  "protocol": "openai",
  "baseURL": "https://api.deepseek.com",
  "apiKey": "Replace with your API Key",
  "model": "deepseek-v4-flash"
}

If you use other services compatible with the OpenAI interface, keep protocol unchanged, you only need to modify the following three fields:

⚠️ Warning: provider.json contains a private key, absolutely do not commit it to Git!

So we create a .gitignore file in the root directory:

Create .gitignore:

node_modules
dist
provider.json

💡 Fun Fact

You might be curious: there are hundreds of large models on the market, do we have to learn hundreds of calling methods? Actually, no. Currently, API calls in the industry are basically unified by two major "common languages": the OpenAI protocol and the Claude protocol. Especially the OpenAI protocol, the vast majority of domestic mainstream models (such as DeepSeek, GLM, Tongyi Qianwen, Kimi, Doubao, etc.) are perfectly compatible with it.

So, once you learn the OpenAI access method, you can seamlessly switch to almost any domestic model by slightly changing baseURL and apiKey. This is why we only write these three fields in the configuration.


5. First, Write a Module to Read the Configuration

Create the src source code directory under powercode:

Create powercode/src/config.ts:

import { readFile } from "node:fs/promises";

/**
 * Provider configuration interface
 * Defines the configuration parameters required to communicate with the AI provider
 */
export interface ProviderConfig {
  baseURL: string;   /** Base URL address of the API service */
  apiKey: string;   /** API key used for authentication */
  model: string;    /** Name of the AI model to use */
}

/**
 * Load and validate the provider configuration
 *
 * @returns Promise<ProviderConfig> Complete provider configuration object
 * @throws Throws an error if provider.json is missing or required fields are incomplete
 * @example
 * ```ts
 * const config = await loadConfig();
 * // Use the configuration to initialize the AI client
 * ```  
 */
export async function loadConfig(): Promise<ProviderConfig> {
  const content = await readFile("provider.json", "utf8");
  const config = JSON.parse(content) as Partial<ProviderConfig>;

  if (!config.baseURL || !config.apiKey || !config.model) {
    throw new Error(
      "provider.json configuration is incomplete; baseURL, apiKey, and model are required.",
    );
  }

  return {
    baseURL: config.baseURL,
    apiKey: config.apiKey,
    model: config.model,
  };
}

It tells TypeScript: the configuration object must contain these three string fields.

To put it plainly, this is equivalent to the "three-piece set" you need to make a call to the AI:

With these three things ready, the next step is to actually send a request to the large model and get it to work.


6. Encapsulate a Large Model Request

Create src/chat.ts:

import OpenAI from "openai";
import type { ProviderConfig } from "./config.js";

export class ChatClient {
  private readonly client: OpenAI; // OpenAI client instance

  /**
   * Initialize the ChatClient instance
   *
   * @param config Provider configuration object
   */
  constructor(private readonly config: ProviderConfig) {
    this.client = new OpenAI({
      apiKey: config.apiKey,
      baseURL: config.baseURL,
    });
  }
  
  /**
   * Send a question to the AI model and get an answer
   *
   * @param prompt User's question or instruction
   * @returns Content of the model's answer
   */
  async ask(prompt: string): Promise<string> {
    const response = await this.client.chat.completions.create({
      model: this.config.model,
      messages: [
        {
          role: "system",
          content: "You are power-code, a hardcore R&D assistant driven by harness engineering. Please answer in Chinese.",
        },
        {
          role: "user",
          content: prompt,
        },
      ],
    });

    return response.choices[0]?.message.content ?? "The model did not return any content.";
  }
}

Let's explain this code in plain language first.

OpenAI is the client provided by the official SDK. Even if you are not connecting to OpenAI, as long as the model service you are connecting to is compatible with the OpenAI interface format, you can usually call it like this:

this.client = new OpenAI({
  apiKey: config.apiKey,
  baseURL: config.baseURL,
});

The core of actually sending the request is:

this.client.chat.completions.create(...)

We put the conversation content into the messages array:

messages: [
  {
    role: "system",
    content: "You are power-code, a hardcore R&D assistant driven by harness engineering. Please answer in Chinese.",
  },
  {
    role: "user",
    content: prompt,
  },
]

Where:

Currently, we only have one question and one answer, so only system and user are needed.


7. Write the Command Line Entry Point

Create src/main.ts:

import { ChatClient } from "./chat.js";
import { loadConfig } from "./config.js";

/**
 * Extract the user's question from command-line arguments
 *
 * @param args Command-line argument array
 * @returns The user's question or instruction
 * @throws Throws an error if the -prompt parameter is missing or empty
 */
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("Content after -prompt cannot be empty.");
  }

  return prompt;
}

/**
 * Main function, processes command-line arguments and calls ChatClient to send the question
 */
async function main() {
  const prompt = getPrompt(process.argv.slice(2));
  const config = await loadConfig();
  const client = new ChatClient(config);

  console.log("AI is thinking...\n");

  const answer = await client.ask(prompt);

  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);
});

The key here is a built-in array provided by Node.js: process.argv. It collects every piece of text we type on the command line.

When we execute this command:

npm run start -- prompt "Hello"

💡 Tip: The -- in the middle is to tell npm: "Please pass the following parameters to my code untouched, don't process them yourself." The complete process.argv array received inside the program actually looks like this:

[
  "/usr/local/bin/node",       // Item 0: The path to the Node.js environment
  "/your/project/path/dist/main.js", // Item 1: The path of the currently executing script
  "-prompt",                   // Item 2: The parameter name we passed
  "Hello"                      // Item 3: The parameter content we passed
]

As you can see, the first two items are system path information, which are useless to us.

So, we need to use .slice(2) to cut off the first two items like slicing a cake, keeping only the real parameters starting from index 2:

["-prompt", "Hello"]

After getting this clean array, the job of the getPrompt function is very straightforward: find the position of -prompt in the array, and then extract the sentence immediately following it (which is "Hello"). This is the actual question the user wants to ask the AI.


8. Compile and Run

Everything is ready. Now we need to convert the TS code into JS code executable by Node.js and get the Agent running.

1. Compile the Code

Execute the build command to compile the TypeScript code under src to the dist directory:

If there are no errors, a dist directory will appear in the project:

powercode/
├── dist/
│   ├── chat.js
│   ├── config.js
│   └── main.js
└── src/
    ├── chat.ts
    ├── config.ts
    └── main.ts

2. Get the Agent Running

Next, call our system and pass in your first instruction:

npm run start -- -prompt "Explain what TypeScript is in one sentence"

image.png

Congratulations, you have written your first command-line AI assistant.


9. What Did We Just Accomplish?

Drawing the whole thing as a data flow, it is:

image.png

The current project structure is also very simple:

powercode/
├── src/
│   ├── config.ts    # Reads and validates model configuration
│   ├── chat.ts      # Requests the large model
│   └── main.ts      # Command-line entry point
├── provider.json    # Local model configuration, not committed
├── .gitignore
├── package.json
└── tsconfig.json

Each file has only one responsibility:

This separation approach will become increasingly important later. Because when we add capabilities like reading files, modifying code, and executing commands, the project won't easily turn into a tangled mess.


11. Does This Count as an AI Agent?

Strictly speaking, not yet.

The current program only has:

image.png

But an Agent that can truly help us complete programming tasks needs at least:

image.png

This is also the problem to be solved in the next article.

In the next article, we will give this assistant a "toolbox," allowing the AI not just to answer questions, but to actively request to read files in the project. At that step, it will truly begin to have the flavor of an Agent.

Comments

Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.

用户98261337817

So detailed!! [grin]

不一样的少年_

[heart][heart]