Build your own agentic framework – the no-magic version

k1r1111 pts0 comments

Build your own agentic framework — the no-magic version | by Kirill | May, 2026 | MediumSitemapOpen in appSign up<br>Sign in

Medium Logo

Get app<br>Write

Search

Sign up<br>Sign in

1. The provider call<br>2. System prompt and user prompt<br>3. Tools — basically just RPC<br>4. The agent loop<br>5. Tokens — counting and the I/O difference<br>6. When does the loop stop?Force structured output<br>Force a tool call<br>When the model gets “stuck”

So… it’s just this?

Build your own agentic framework — the no-magic version

Kirill

10 min read·<br>May 17, 2026

Listen

Share

(in around 130 lines of vanilla JS)<br>Press enter or click to view image in full size

When I was a young PHP developer, we had a running joke: every PHP developer had to write their own CMS.<br>You start as a junior, building plain PHP sites. At some point, the customer asks for an admin panel, so you reach for Joomla or Drupal or whatever’s hot that month. Then one day you start feeling like you could do this better yourself — better UX, better API, better ORM, pick your axis. So you sit down and build your own CMS.<br>And it teaches you a lot . How the whole thing actually works under the hood. How the pieces connect. How you handle auth and sessions. How a template engine plugs in. How to keep it from being a security disaster.<br>I was one of those developers.<br>Now, in 2026, I have the same itch — but for agents. I use existing agentic frameworks all the time. LangChain in Python. Vercel’s AI SDK in JS. They’re fine. But sometimes they do weird things. Sometimes the API feels counterintuitive. Sometimes I just want more control than they give me.<br>So maybe it’s time to build one from scratch.<br>Not a real framework — a small one. Just enough to understand. Vanilla JS. Node.js (so we have “fetch” built in). No npm install. ESM modules. By the end, you’ll have around 130 lines of code in a single “agent.mjs” file, and you’ll know what every single line does.<br>A note before we start. The code below uses the OpenAI-compatible Chat Completions API . I’ll read the base URL, the API key, and the model name from environment variables, so the same code works against OpenAI’s endpoint, Moonshot’s Kimi endpoint (which I happen to use because it’s cheaper), or anyone else who exposes a compatible API. If you ever need to plug in a provider that isn’t OpenAI-compatible — Anthropic’s native API, for example — you’d add an adapter layer that translates between provider shapes. We’re not doing that today, but keep it in mind. That’s exactly the abstraction layer LangChain spends a lot of code on.<br>1. The provider call<br>Step one — the only piece you actually can’t skip. Talk to the model.<br>Create “agent.mjs” and start with this:<br>const BASE_URL = process.env.OPENAI_BASE_URL || 'https://api.moonshot.ai/v1';<br>const API_KEY = process.env.OPENAI_API_KEY;<br>const MODEL = process.env.MODEL || 'kimi-k2.6';

async function chatComplete({messages, tools, tool_choice, response_format}) {<br>const body = {<br>model: MODEL,<br>messages,<br>};

if (tools) {<br>body.tools = tools;

if (tool_choice) {<br>body.tool_choice = tool_choice;

if (response_format) {<br>body.response_format = response_format;

const resp = await fetch(`${BASE_URL}/chat/completions`, {<br>method: 'POST',<br>headers: {<br>'Content-Type': 'application/json',<br>Authorization: `Bearer ${API_KEY}`,<br>},<br>body: JSON.stringify(body),<br>});

if (!resp.ok) {<br>throw new Error(`API error ${resp.status}: ${await resp.text()}`);

return resp.json();<br>}That’s it. The whole “provider layer.” Nothing here you don’t already know — POST JSON, parse JSON back.<br>If you ever want to support a provider that doesn’t speak the OpenAI shape (Anthropic’s native API, Google Gemini, etc.), this function is the natural place for a switch — and that switch is what people call the adapter layer . For now, we don’t need one. One file, one provider format. You can find detailed OpenAI API documentation here.<br>2. System prompt and user prompt<br>The Chat Completions API takes a list of messages, each with a “role” and “content”. The roles you care about:<br>“system”— your instructions to the model. Set the persona, the rules, and the expectations.<br>“user” — what the user actually said.<br>“assistant” — what the model already said in this conversation. You’ll append these as you go.<br>“tool” — the response from a tool call (more about it in a second).<br>Smallest possible test — drop this at the bottom of “agent.mjs”:<br>const resp = await chatComplete({<br>messages: [<br>{ role: 'system', content: 'You are a helpful assistant. Be concise.' },<br>{ role: 'user', content: 'What is the capital of France?' },<br>});<br>console.log(resp.choices[0].message.content);Run “OPENAI_API_KEY=sk-… node agent.mjs”. You get “Paris” or similar.<br>Press enter or click to view image in full size

That’s it. That’s the foundation. Everything else in this article is just layers on top of “chatComplete”.<br>3. Tools — basically just RPC<br>Here’s the part that confused me the first time I saw it: tool calling sounds magical, but it’s actually just RPC. The model says, “I want to...

model provider resp tools const body

Related Articles