Eli Zibin

We Built a Minimal Agent REPL

16/02/2026

agents repl typescript

Agents as While Loops: using a tiny terminal REPL to build a simple tool-calling agent.

This is a small and simple learning exercise: make a miniature agent runtime that mimics the fundamentals of a real agent loop. No framework magic, just one clear loop: user input, model output, tool calls, repeat. I was inspired by this tweet below:

A compact way to describe why agent runtime building mostly comes down to loops and structure.

System image

System view of the minimal agent REPL architecture with provider and local tools.
One-screen view: terminal REPL, provider API, and local tools tied together by a tool-call loop.

Live demo

Quick run from the real REPL.

Terminal recording of the agent REPL streaming output, running a tool call, and returning a final answer.
Live terminal pass: prompt -> streamed response -> tool call -> final answer.

Loop diagram

Agent loop diagram showing responses requests, function calls, tool execution, and resume.
Two loops: the outer REPL loop and the inner tool-calling loop.

What we built:

1. Streaming assistant output in the terminal so we can see progress in real time.

2. A thinking indicator before first token so slow turns still feel responsive (super basic).

3. Session save/load so conversations can be resumed.

4. Safe workspace inspection tools for reading and searching local files.

5. Per-turn metrics plus Ctrl+C cancellation.

6. Phase-based reasoning controls for plan/build/review modes.

Safety guardrails

We intentionally kept tool access constrained.

- read_workspace_file blocks absolute paths and parent directory traversal.

- search_workspace skips .git and node_modules.

- Both tools cap bytes/results and skip binary content.

Provider flexibility

The same loop runs on OpenAI or OpenRouter. We can switch providers and models at runtime with /provider and /model, which made testing and comparison much easier.

The tiny tool-calling loop that matters

TS

let response = await responses.create({
  input: history,
  tools,
  stream: true,
});

history.push(...response.output);

while (hasFunctionCalls(response.output)) {
  for (const call of getFunctionCalls(response.output)) {
    const result = await runTool(call.name, JSON.parse(call.arguments));
    history.push({
      type: "function_call_output",
      call_id: call.call_id,
      output: JSON.stringify(result),
    });
  }

  response = await responses.create({
    input: history,
    tools,
    stream: true,
  });

  history.push(...response.output);
}
Minimal tool-call loop: run tool calls, append outputs, continue response.

Metrics screenshot

Single-turn trace with both tool execution and per-turn metrics.

Terminal screenshot showing tool call lines and meta metrics output for a completed REPL turn.
One frame showing tool> activity and meta> status, request count, and token metrics.

Conclusion

This was fun and educational: small, understandable, and practical enough to understand. Next step is looking at other more advanced agent runtime features and sdks, but the core loop is working and feels good to build on. Thanks for reading!