A resumable, human-in-the-loop AI agent in ~200 lines with zero dependencies A developer created yieldagent, a minimal AI agent library in ~200 lines with zero dependencies, featuring human-in-the-loop pause/resume and testability without an LLM. The library exposes every step as a plain object via async generators, allowing callers to log, render, or assert on tool calls and results. It supports serializable resume states for pausing and resuming agent runs across different requests or restarts. Most "AI agent" libraries fall into one of two buckets. Either they're a big framework you spend an afternoon configuring, or they're a tiny toy that drops the one feature you actually need in production: the ability to stop and ask a human before the agent does something you can't undo. I wanted the middle. So I wrote yieldagent https://github.com/rahul1368/yieldagent : a small agent loop you can read end to end, with human-in-the-loop pause/resume built in, and no runtime dependencies. This post walks through how it works and why it's built the way it is. Strip away the branding and an "agent" is a loop: That's it. The model decides the control flow at runtime; your job is to run the tools and feed the results back. Here's the core, lightly trimmed: js for let step = 0; step < maxSteps; step++ { const reply = await call messages, toolSpecs ; messages.push reply ; if reply.tool calls?.length { yield { type: "final", text: reply.content, messages }; return; } for const tc of reply.tool calls { const args = JSON.parse tc.function.arguments ; const result = await tools tc.function.name .run args ; messages.push { role: "tool", tool call id: tc.id, content: JSON.stringify result } ; } } Everything else in the library is in service of making this loop observable, testable, and safe to run against the real world. Notice the yield . The loop is an async generator, so the caller drives it: js for await const step of agent { call, tools, messages } { if step.type === "tool-start" console.log "- ", step.tool, step.args ; if step.type === "final" console.log step.text ; } Every step each tool call, each result, and the final answer is handed back to you as a plain object. Nothing is hidden inside the framework. You can log it, render it, or assert on it in a test. Which brings us to the nicest side effect. The model call is just a function: messages, tools = Promise