We dont need no subscriptions
Transcript
[00:00] In this video we are going to create a very basic agent loop with some very simple tools. This is very much what something like cloud code or whatever is based on. This is the type of thing you’re going to see with pi coding agents and things of that nature, but we’re just going to build it from scratch. So what I’ve got going on here I’ve got the ai stk, I’ve got ai, I’ve got zod which we’re going to use a little later, and I’ve got this bin here because I want to be able to run this from anywhere on my system.
[00:39] We’re primarily going to be working in one file, this is our index ts. Got a few things we’re bringing in from ai stk: we’ve got create open ai, we’ve got the generate text command, this is loop finish which we’ll talk about, model message type, and from node we’ve just got read line which is going to act as our interface. Set up a couple little constants here, I’m going to be using this model because it happens to run really well on a mac. We’re setting up our uh api here, calling it a llama, create open api here’s our url and here’s our key that we don’t really need.
[01:19] We are going to start with our agent loop, so our agent loop is going to be an async function that takes in messages which will be an array of those model messages that we got from the ai stk. And then using ai stk uh we are going to say we’re going to get the text response from generate text. That guy is going to take in an object, going to have our model which is going to be a llama which was the api we set up dot chat, and then our model. It’s going to have a system prompt figure that out in a second, then we’re going to send in our messages which are coming in as an argument and then we’re going to use this stop when.
[02:06] And this is where we’re going to use is loop finished, and this is like some real magic. It ends up saving you a whole bunch of looping logic that you would have to build out. This basically says let the llm run until it decides that it’s finished or until the user says so. The next thing that we’re going to have here and we’re going to skip for just right now is going to be tools. We’re going to come back to that because we haven’t built those yet, so our system prompt is going to be you are a coding agent at and then we’ll just throw in that working directory.
[02:48] This guy right here which is our current working directory, that’s going to allow us to use this in other directories later so we’ll come back to that. Okay uh so once we get past that guy I think we’re just going to return our text. Let’s type a return promise of string there we go, let’s await this guy all right our agent loop is looking good. We’re going to jump to our interface now. Our interface like I said it’s just going to use read line so we’re going to set that up with rl equals read line dot create interface and that this is pretty much always the same. We’re going to have input which is going to be process dot standard in, now we’re going to have output which is going to be process dot standard out.
[03:36] Okay so there’s our interface, we’re also going to have um our messages that we carry around that we’re going to pass into our loop. I’m going to call that history, I’m going to set that up to be model message array and for now that’s going to be empty. Okay now we’re going to have our prompt and this is going to be uh like a recursive function that just keeps coming back to the prompt, like what, what do you want to do next? Uh so that’s going to be a function that just voids this out or I’m sorry it doesn’t return anything. And then we’re going to use our read line to ask a question and we’re just going to have some sort of generic little prompt here. I’m going to say input and uh whatever that’s you know where you were expecting the user to type something after that.
[04:24] Uh so we’re going to have our async, we’re going to be passing in we’ll call it our query which is the uh message that the user gives us. First thing we’re going to do is push that into our history. So uh if you’ve seen any of my videos before this message is right here is usually an array of objects, you have a role, you have a type and then you have the content things of that nature. So here we’re going to say this role is the user and the content is the query, so we are going to do that one more time with a little guy in between. But for now this is going to be um role is our assistant so this is where we’re going to get our reply, so we’ll be sending the reply there.
[05:12] So now we need to get that, so const reply and this is really easy as a weight agent loop history. So we’re just passing in all the messages nice and easy. Okay so once we get past that we’re going to say if we in fact have a reply we’re going to log that guy out and then we’re just going to log out an empty line in order to start the next prompt. So we’re done and now we want to prompt again. And then to kick this whole thing off we’re just going to run prompt, so that’s that’s essentially going to run forever. We could add in here like if the query is uh the word exit or something to go ahead and exit, but for right now you just ctrl c uh to get out of this guy.
[06:02] Um so technically technically we could try this out right now, so I’m just going to do that. Let me zoom this way in say fun run index and we’ll see if I mess that up. There’s her input and I say what is two plus two and we get two plus two is four. Uh who was the fifth u.s president? James Monroe yada yada yada. And since we have our history I should be able to say who was next and who was after him. Cool the president after James Monroe Monroe was uh John Quincy Adams I believe it. I don’t know if it’s true or not but that’s not why we’re doing this uh but either way we have we have a groovy little interface here. Now the thing is if I start asking it to do things create files or whatever it can’t do that, so that’s where our tools are going to come in.
[07:06] Let me move this out of the way uh we are going to need to bring in a couple more things. So we got generate text we got this guy from ai, we’re going to bring in tool and we’re also going to bring in zod schema which is just a shortcut available to us from the ai library that makes it easier for us to define these schemas very quickly. We’re also going to import zod itself so z from zod. Uh we are going to need a way to spawn a uh command line so import spawn sync. Okay I think this is probably all we need, we’ll we’ll see. Um I’m going to drop something in here that I sketched out earlier and that’s going to be block commands. We kind of don’t want it to run some of these commands so we’ll have that there. Now when we get to our tools uh we’re actually going to structure this in two ways.
[08:06] We’re going to have our actual code so that’s going to be like a function called run bash which is going to be what our tool can run. So we’ll have that and then we’ll have this object called tools where we could add more tools to it later. And the first one will have a key of bash and then it’ll use the tool command from the library and that’ll take in an object. It’ll have a description and say run a shell command. Now I have an input schema so any gravity kickback and that looks pretty good, which will have command which would be z dot string. And again I’m gonna tell that guy to shut up for a minute. Okay so after the input schema we’re going to have our execute and this is where we say like go run the thing.
[09:19] So async it’s going to take in a command command be a string. Then all we got to say is const we’ll call it output is equal to run bash command and then we’ll just return the output there we go. Okay so we got our tools and in the end we are going to come down here and say tools, so that’s that’s how we’re going to get that to our agent loop. Okay so now we need to write this run bash guy. So we just saw that it’s going to take in a command which is going to be a string and this guy is going to return a string. And then we’re going to say if what we call it block commands. So first thing we want to do is make sure we’re not running some of those commands so sum.
[10:24] So if we have that then we are just going to return, we’ll just return a string with an error finger little Robinson whatever. Uh okay so now now we want to actually run our guys so we’re going to do a try catch on this. So catch our error we’ll return there, I’m not going to worry about this syntax right now. Um okay so now we just need to get our actual result which is going to be. Now we’re going to use that spawn sync but we just break out the command. So we’re going to run shell and then we’re going to run taxi which is going to be our command. And then the actual command after that we have some options to tell it where to do all this. So we’re going to say our current work in directory is work directory, we’re going to say our encoding is utf8 and we’re going to set a general timeout here of like two minutes.
[11:44] Yeah okay now where did I now result equals here we’re going to return. We’re going to concatenate our result dot standard out and if we have it our result that standard error trim those guys up. And then we’ll make sure that we’re inside of let’s say 50 000 characters otherwise we’ll return nothing. That looks good to me I’m sure I messed something up. Let’s see I got some missing we don’t need that. There we go let me clean all this up. Okay so we’ve got a run bash which hopefully works, we’ve got our tools so we’ve got bash. And then here we’re going to say uh use bash to solve tasks act don’t explain.
[12:56] All right I think we’re I don’t know it’s worth a try right. Okay we’re gonna zoom this terminal way in again we’re gonna say bun run index. We get our input, let’s start with the simple one what is two plus two cool. Now we’re gonna try to do something so let’s say we’re gonna create a file called hello dot ts. Its content should be a function that logs out hello world. We’ll see how that goes I don’t have high hopes to be honest. Yeah you know what that looks pretty good to me, there’s our hello well. Let me zoom this way out okay it’s a function it does what it’s supposed to do.
[13:57] Cool uh so what have we done? We’ve created an agent that can use tools, we’ve got an agent loop that’ll run through until it’s done, it uses bash and we can add a whole bunch more to this. But one thing I am going to do is show you I mentioned earlier on that I had this whole bin thing set up here so that I could use it in other places. So what we’re going to do is kill this and then I’m going to say bun link. And now my jeevs project should be available in other uh directories. So what I’m going to do let’s bring up a separate terminal here and I’ll zoom this guy way in so you can see. And I’m just going to say make directory um test jeevs and I’ll zoom that in a bit more sorry.
[14:51] Okay so we’ll go into this test jeevs directory there’s nothing in there it’s empty. And so what I’ll do is I’ll say jeevs and there’s my input. So now my working directory is test jeevs so if I say create a file called uh you know I’ll just use the same one I did earlier. Open up a new tab and list that out there’s our hello ts cad hello. See us and there’s our function that called says hello world. So this works, I can take it around, I can add a whole bunch more tools. And there’s actually I think I’m going to follow up on this video with another one where we do uh explain why you might want more than just bash. There is a world where some people say all you need is bash um but there are certainly reasons you might want more specified tools.
Here we’ll build a CLI-based AI coding agent that can execute bash commands to help you with development tasks.
Prerequisites
- Bun runtime installed
- Ollama running locally with a model
- qwen3.5:35b-a3b-coding-nvfp4 is the newest hotness
- Node.js basics familiarity
The Code
Here’s the complete implementation in index.ts:
#!/usr/bin/env bun
import { createOpenAI } from "@ai-sdk/openai";import { generateText, isLoopFinished, tool, zodSchema } from "ai";import type { ModelMessage } from "ai";import { z } from "zod";import { spawnSync } from "child_process";import * as readline from "readline";
/** CONSTANTS */const WORKDIR = process.cwd();const MODEL = "qwen3.5:35b-a3b-coding-nvfp4";const BLOCKED_COMMANDS = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"];
/** API */const ollama = createOpenAI({ baseURL: "http://localhost:11434/v1", apiKey: "ollama",});
/** TOOLS */const runBash = (command: string): string => { if (BLOCKED_COMMANDS.some((c) => command.includes(c))) { return "Error: Danger Will Robinson!!!"; } try { const result = spawnSync("sh", ["-c", command], { cwd: WORKDIR, encoding: "utf8", timeout: 120000 }); return (result.stdout + result.stderr).trim().slice(0, 50000) || "";
} catch (e) { return `Error ${e}`; }};
const TOOLS = { bash: tool({ description: "Run a shell command", inputSchema: zodSchema(z.object({ command: z.string() })), execute: async ({ command }: { command: string; }) => { const output = runBash(command); return output; } })};
/** AGENT LOOP */
const agentLoop = async (messages: ModelMessage[]): Promise<string> => { const { text } = await generateText({ model: ollama.chat(MODEL), system: `You are a coding agents at ${WORKDIR}. Use bash to solve tasks. Act, dont explain.`, messages, tools: TOOLS, stopWhen: isLoopFinished(), }); return text;};
/** INTERFACE */
const rl = readline.createInterface({ input: process.stdin, output: process.stdout});const history: ModelMessage[] = [];
const prompt = (): void => { rl.question(" input >> ", async (query) => { history.push({ role: "user", content: query }); const reply = await agentLoop(history); history.push({ role: "assistant", content: reply }); if (reply) console.log(reply); console.log(); prompt(); });};
prompt();How It Works
1. API Configuration
The code creates an OpenAI-compatible client pointing to your local Ollama instance:
const ollama = createOpenAI({ baseURL: "http://localhost:11434/v1", apiKey: "ollama",});2. Tool Definition
We define a bash tool that the model can call to execute shell commands. The tool uses Zod schema validation to ensure proper input:
const TOOLS = { bash: tool({ description: "Run a shell command", inputSchema: zodSchema(z.object({ command: z.string() })), execute: async ({ command }) => runBash(command) })};3. Security Measures
A blocklist prevents dangerous commands from executing:
const BLOCKED_COMMANDS = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"];4. Agent Loop
The agentLoop function uses generateText with stopWhen: isLoopFinished() to automatically handle tool execution cycles. The model will keep calling tools until it completes the task without needing more input.
const agentLoop = async (messages: ModelMessage[]): Promise<string> => { const { text } = await generateText({ model: ollama.chat(MODEL), system: `You are a coding agents at ${WORKDIR}. Use bash to solve tasks. Act, dont explain.`, messages, tools: TOOLS, stopWhen: isLoopFinished(), }); return text;};5. Interactive CLI
The readline interface provides a simple interactive prompt that maintains conversation history:
const prompt = (): void => { rl.question(" input >> ", async (query) => { history.push({ role: "user", content: query }); const reply = await agentLoop(history); // ... });};Running the Agent
- Ensure Ollama is running with your chosen model
- Run the script:
bun run index.ts- Enter your task at the prompt. For example:
input >> Create a new file called hello.txt with the content "Hello World"The agent will execute the necessary bash commands to complete your task.
Key Concepts
- Tool Execution: Vercel AI SDK’s
toolfunction allows models to execute code and return results - Loop Detection:
isLoopFinished()automatically detects when the model has completed its task - Message History: Maintaining conversation context helps the agent understand ongoing tasks
- Zod Integration: Schema validation ensures tools receive correct input types
This implementation provides a foundation for building more sophisticated AI coding assistants!
This post is now a part of a larger series on building AI agents with TypeScript. Click here to keep building!