cd /news/ai-agents/a-resumable-human-in-the-loop-ai-age… · home topics ai-agents article
[ARTICLE · art-67486] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

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.

read4 min views1 publishedJul 21, 2026

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:

a small agent loop you can read end to end, with human-in-the-loop /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:

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:

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<Message>

. In

tests, you pass one that returns canned replies. No API key, no network, and the

result is deterministic:

const replies = [
  { role: "assistant", content: null, tool_calls: [{ id: "1", function: { name: "getWeather", arguments: '{"city":"Delhi"}' } }] },
  { role: "assistant", content: "It's 31°C.", tool_calls: [] },
];
let i = 0;
const call = async () => replies[i++];

const steps = [];
for await (const s of agent({ call, tools, messages })) steps.push(s);

expect(steps.map(s => s.type)).toEqual(["tool-start", "tool-end", "final"]);

The whole library is tested this way. Agent logic you can unit-test without an

LLM is worth a lot when you're iterating.

Real agents do risky things, send emails, spend money, delete files. You want a

human in the loop before that happens, and often you want to a run and

continue it later, in a different request or after a restart.

yieldagent does this with an approve

callback. Return false

for a tool and

the loop stops before running it, handing back a serializable resumeState

:

const cfg = {
  call, tools,
  messages: [{ role: "user", content: "Email the Delhi weather to my boss" }],
  approve: (tool) => tool !== "sendEmail",
};

let d;
for await (const step of agent(cfg)) {
  if (step.type === "d") d = step.resumeState; // a plain object
  if (step.type === "final") console.log(step.text);
}

Because resumeState

is just data, you can write it to a database or a job

queue, wait for a human to click "approve" somewhere else entirely, then pick up

where you left off:

import { resume } from "yieldagent";
for await (const step of resume(cfg, d)) {
  if (step.type === "final") console.log(step.text);
}

This is the part most minimal agents skip and most big frameworks turn into a

whole subsystem. Keeping it small and explicit was the main reason I wrote this.

The core knows nothing about any specific provider. The included adapter talks

to anything that speaks the OpenAI /chat/completions

shape, OpenAI, Anthropic's

compatible endpoint, Groq, Together, or a local model via Ollama or vLLM:

const call = openaiCompatible({
  baseURL: "http://localhost:11434/v1", // Ollama
  apiKey: "ollama",
  model: "llama3.1",
});

Or skip the adapter and write your own call

, it's about a dozen lines.

stream

instead of call

and you get token

steps as the model produces text. Tools and /resume still work.yieldagent/zod

entry derives the JSON Schema from a Zod schema and validates the model's arguments, feeding errors back so the model can correct itself.AbortSignal

to stop a run on a timeout or user cancel.If you need streaming UI helpers, a big prebuilt tool ecosystem, or multi-agent

orchestration out of the box, reach for the Vercel AI SDK or LangGraph. yieldagent

is for when you'd rather own a loop you can read in a few minutes than adopt a

framework. If you outgrow it, you'll know exactly what you're replacing.

npm install yieldagent

There's a browser demo of the approval flow (no API key needed):

https://rahul1368.github.io/yieldagent/

Code and docs: https://github.com/rahul1368/yieldagent

If you build something with it, or the /resume API breaks down for your use

case, I'd like to hear about it.

── more in #ai-agents 4 stories · sorted by recency
── more on @yieldagent 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/a-resumable-human-in…] indexed:0 read:4min 2026-07-21 ·