Using Mini Me as a library, thanks to ACP
Drive Mini Me from your own Node.js apps over ACP: ask questions, stream answers, allow or deny tools and cancel requests, with the official SDK and no server.

In a post I wrote yesterday, ACP from scratch, I explained how a code editor (Zed, JetBrains, ...) "talks" to a coding agent with the Agent Client Protocol (ACP): the editor launches the agent as a subprocess, and they exchange JSON-RPC messages over stdin/stdout.
But actually, nothing forces the "client" to be a code editor. Any program that can launch a process and write/read lines of JSON can drive an ACP agent. And since Mini Me is an ACP agent (mm -acp), it means you can use Mini Me "as a library", in your own applications:
- ask it questions,
- get its answers as a stream,
- let it use its tools (or not),
- cancel an answer that takes too long,
- ...
🎉 And the good news is that there are already official ACP SDKs in several languages (TypeScript, Kotlin, Java, Python, Rust, ...) and a good handful of community SDKs (Go, Swift, .NET, ...). So no need to code everything by hand.
Today, let's see how it's done, step by step, with the example demo.acp.sdk/nodejs-sdk: a small Node.js library built on the official SDK @agentclientprotocol/sdk.
The principle
Here is the "layered" organisation:
- your application only uses a few functions:
Agent.start(),agent.complete(),agent.stream(), ... - the
lib/minime.jslibrary (~340 lines, comments included) relies on the ACP SDK to talk to Mini Me, - Mini Me (
mm -acp) runs as a child process, with itsagent.yamlconfiguration file (its tools, its skills, its model, ...), - and it's Mini Me that talks to the model (llmman, llama.cpp, Ollama, Docker Model Runner, ...).
✋ No HTTP server, no port, no network: your program and Mini Me are connected by two "pipes".
What you need
- Mini Me installed (the
mmbinary on yourPATH, or named by theMM_BINvariable), - an LLM engine with a model; for my examples I used llmman with
Qwen3.5-4B-GGUF:Q4_K_M, - Node.js 20 or later.
llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M
llmman serve
cd demo.acp.sdk/nodejs-sdk
npm install
npm installinstalls the@agentclientprotocol/sdkSDK (version2.2.0) andzod(which the SDK needs to validate the messages). That's all.
Step by step: how it works "under the hood"
Step 1: launch Mini Me as a subprocess
We start by launching mm -acp with the agent's configuration file:
import { spawn } from "node:child_process";
const child = spawn("mm", ["-acp", "-config", "/absolute/path/to/agent.yaml"], {
cwd: process.cwd(),
stdio: ["pipe", "pipe", "pipe"],
});
📝 This code is a simplified version of the
Agentclass constructor, inlib/minime.js. There, the binary's path comes from themmoption (or theMM_BINvariable), the agent's path from theconfigoption, and the process is stored in a private field of the class (this.#child). The same goes for the code excerpts of the next steps: they are simplified versions oflib/minime.js.
Mini Me now waits for messages on its standard input. Its logs (banner, warnings, ...) go to stderr, and its standard output only carries the protocol.
stdio: ["pipe", "pipe", "pipe"] creates three "pipes", one per standard channel of Mini Me, in the order stdin, stdout, stderr: the first two are for the protocol (we write to stdin, we read from stdout), the third one is for reading Mini Me's logs, which comes in handy to explain an error (for example when the model does not answer).
Step 2: plug the SDK into the "pipes"
The SDK works with Web Streams. Node.js can convert the child process's streams with Writable.toWeb and Readable.toWeb, and the SDK's ndJsonStream function takes care of the "one JSON message per line" framing:
import { Readable, Writable } from "node:stream";
import { client, ndJsonStream } from "@agentclientprotocol/sdk";
const app = client({ name: "minime-sdk" })
.onRequest("session/request_permission", ({ params }) => answerPermission(params));
const connection = app.connect(
ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout))
);
client() creates the "client" side of ACP. With onRequest(...), we declare how to answer the requests Mini Me will send us (we'll come back to it later, in step 5). For all the other requests (read a file, open a terminal, ...), the SDK answers "method not found" on its own (since we did not advertise them).
Step 3: introduce yourself 👋 and open a session
As with a code editor (Zed for example), there is first the "handshake" 🤝 (initialize), then the opening of a session (session/new):
import { PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
await connection.agent.request("initialize", {
protocolVersion: PROTOCOL_VERSION,
clientCapabilities: {},
clientInfo: { name: "minime-sdk", version: "0.1.0" },
});
const session = await connection.agent.buildSession(process.cwd()).start();
clientCapabilities: {}: we don't advertise any service (files, terminal, forms). Mini Me runs its tools itself.buildSession(cwd).start()sendssession/newand returns anActiveSession: it's the one that will collect, in order, all thesession/updatenotifications of this session.
Step 4: ask a question and read the answers as they come
We send the prompt with session.prompt(...), then we read the messages one by one with session.nextUpdate(), until the end message (stop):
session.prompt("In one sentence: what is a JSON-RPC notification?");
let text = "";
for (;;) {
const message = await session.nextUpdate();
if (message.kind === "stop") {
console.log(`\n[${message.stopReason}]`);
break;
}
const update = message.update;
if (update.sessionUpdate === "agent_message_chunk" && update.content.type === "text") {
process.stdout.write(update.content.text); // a chunk of the answer
text += update.content.text;
}
}
In the library, each session/update is "reduced" to an event that is simpler to use: text, thinking, tool_call, tool_result, and finally done (with the stop reason, the tokens used, and the whole answer). The other updates (usage_update, available_commands_update, ...) are details for editors, we ignore them.
Step 5: answer the permission requests
If your agent is configured with yolo: false, Mini Me asks for permission before each tool (for example before running a bash command). It's a request that goes from Mini Me to your program: session/request_permission, with a list of options (allow, allow_always, reject, never). You just send back the chosen option:
async function answerPermission(params) {
const allowed = await onPermission(params.toolCall); // your decision
const kind = allowed ? "allow_once" : "reject_once";
const optionId = params.options.find((o) => o.kind === kind)?.optionId;
return { outcome: { outcome: "selected", optionId } };
}
✋ In the library, without an onPermission callback, everything is refused: nothing runs unless you decided it.
Step 6: cancel
To stop an answer that takes too long, we send the session/cancel notification. Mini Me stops the generation and still answers the prompt, with stopReason: "cancelled":
await connection.agent.notify("session/cancel", { sessionId: session.sessionId });
Step 7: let's wrap it all up
The lib/minime.js library puts all these steps together in an Agent class. And so, here is what it looks like on the application side:
import { Agent } from "./lib/minime.js";
const agent = await Agent.start({ config: "agent.yaml" });
const { text, stopReason, usage } = await agent.complete("In one sentence: what is a JSON-RPC notification?");
console.log(text);
console.log(`\n[${stopReason} · ${usage?.totalTokens ?? "?"} tokens]`);
await agent.close();
The result (the complete.js example also prints the answer's duration):
A JSON-RPC notification is a JSON-RPC message that has no response, used to send asynchronous events or commands from a client to a server without expecting a reply.
[end_turn · 663 tokens · 7.3 s]
And in "streaming" mode, with a permission callback that only allows "read-only" commands:
const readOnly = /^(ls|find|cat|wc|head|tail|pwd|grep)\b/;
const agent = await Agent.start({
config: "agent.yaml",
onPermission: (call) => {
const ok = readOnly.test(call.input?.command ?? "");
console.log(`\n🔐 ${call.title} → ${ok ? "allowed (read-only)" : "refused"}`);
return ok;
},
});
for await (const ev of agent.stream("How many .js files are in the test folder? Use a command.")) {
if (ev.kind === "text") process.stdout.write(ev.text);
if (ev.kind === "tool_call") console.log(`🛠️ ${ev.title}`);
if (ev.kind === "tool_result") console.log(` ${ev.status}: ${String(ev.output).trim()}`);
if (ev.kind === "done") console.log(`\n[${ev.stopReason}]`);
}
🔐 bash: find test -name "*.js" | wc -l → allowed (read-only)
🛠️ bash: find test -name "*.js" | wc -l
completed: 2
There are 2 .js files in the test folder.
[end_turn]
✋ A check on the start of the command is for the demo, it is not a "sandbox":
find . -deleteorls; rm xwould pass without any problem, so be very careful with what you do (my advice: work in a sandbox, a container, a VM).
To cancel, it's a simple AbortController:
const ac = new AbortController();
setTimeout(() => ac.abort(), 3000);
const res = await agent.complete("Write a 2000-word story about a lighthouse keeper.", { signal: ac.signal });
// res.stopReason === "cancelled", res.text holds the beginning of the story
[cancelled after 3.0 s, 366 characters kept]
There are 4 examples to run (node complete.js, node stream.js, node cancel.js and node permission.js), and tests (npm test) that run without a model, thanks to a "fake" Mini Me that plays the agent side of ACP.
A few rules to remember:
- 1 Agent = 1
mmprocess = 1 conversation: each call continues the conversation, as in an editor (agent.reset()to start from scratch). If you need parallelism, start several Agents. - A cancelled question is dropped from the conversation by Mini Me: the next question won't know anything about it.
- A refusal doesn't stop a small model from guessing: in the
permission.jsexample, if we refusepwd, the model still answers.../root(it made up the answer).
And now? The perspectives
This is where it gets interesting (well, for me 😉). If an application can drive a coding agent with a few lines of code, then any application can "embed" an agent that knows how to write and run code, and not only code editors.
A few ideas, for "consumer" applications:
- a note-taking application: "turn my meeting notes into a table", "sort these files by project". The agent writes the script, asks for permission, runs it.
- a bot for the family (on Discord, Matrix, ... with Mini Me running on a mini PC at home): "rename the holiday photos by date", "make me a PDF with this week's photos".
- a spreadsheet or a data analysis tool: "make me a chart from this CSV". The agent generates the Python or JavaScript code that does the job, instead of trying to "guess" the result.
- home automation: "write the rule that turns the lights off at 11 pm". The agent knows the rules' format thanks to a skill, and shows you what it is going to write before doing it.
- an application to learn to code: a local "tutor" that explains your code step by step, without your code going to the cloud.
- small business tools (an association, a craftsman, a small company): "fix the invoice export script", without needing a developer for every small change.
- 🤫 I have a little project incubating for very soon 🤐
What I like about this approach when I use it with Mini Me:
- we can bring the power of coding agents to non-technical users.
- everything stays local: the model runs on your machine, your data (photos, notes, invoices, ...) goes nowhere. And it costs nothing in tokens.
- the application stays in control: the permission callback is your application's dialog box. The user sees what the agent wants to do, and decides.
- one specialised agent per application: with the
agent.yamlfile (the model, the tools, the skills, the hooks, ...), each application can have "its own" Mini Me, tailored to its use, with a "tiny model" that is largely enough for targeted tasks. - it's a standard: the application speaks ACP. In theory, with very few changes, it could drive another ACP agent (Gemini CLI, Claude Code, ...).
Of course, there are constraints: the mm binary must be installed on the machine, and everything runs locally (stdio).
References
The clients are all available in the Mini Me repository, with the same API, four examples (not streamed, streamed, cancellation, permission) and tests:
demo.acp.sdk/nodejs-sdk: Node.js with the official ACP SDK@agentclientprotocol/sdk(this post's example)demo.acp.sdk/nodejs: Node.js without an SDK and without any dependency, the JSON-RPC protocol is written "by hand"demo.acp.sdk/go: Go, with the community SDKgithub.com/coder/acp-go-sdk(the same one Mini Me uses)demo.acp.sdk/kotlin: Kotlin (JVM), with the official ACP SDKcom.agentclientprotocol:acpand coroutines
And to go further:
- ACP from scratch: yesterday's post on my blog, to understand the protocol
- Agent Client Protocol: the protocol's official website, with the list of official and community SDKs
@agentclientprotocol/sdk: the official TypeScript SDK
That's all for today. See you very soon 👋.
Written by
Keep reading
Mini Me and the ACP protocol (Agent Client Protocol)
Plug Mini Me into Zed and VSCode through the Agent Client Protocol, directly or running inside a Docker sbx sandbox, with a ready-made translator agent.

Hello Mini Me: an agent born while preparing a talk for Devoxx
Meet Mini Me, a coding agent born from a Devoxx talk on the agent loop, built for small local models with guardrails against loops and hallucinations.

Building a team of Mini Mes with ACP
Build a team of local Mini Me agents talking over ACP: a coordinator that routes your Go and Rust questions to two documentation specialists.

No comments yet. Be the first to comment!