mini-me from Node.js, with the official ACP SDK: lib/minime.js
A Node.js client that uses mini-me as a library by driving mm -acp over ACP (ticket 188), built on the official ACP TypeScript SDK, @agentclientprotocol/sdk 2.2.0. It has the same API as the dependency-free client in ../nodejs/, which speaks JSON-RPC by hand: the examples of both folders are the same programs. The library is JavaScript (ESM) with JSDoc types, so there is no build step. See ../README.en.md for the ACP messages behind it.
import { Agent } from "./lib/minime.js";
const agent = await Agent.start({ config: "agent.yaml" });
const { text } = await agent.complete("Explain this repository"); // not streamed
for await (const ev of agent.stream("And the tests?")) { // streamed
if (ev.kind === "text") process.stdout.write(ev.text);
}
const ac = new AbortController();
setTimeout(() => ac.abort(), 3000);
await agent.complete("A long task", { signal: ac.signal }); // cancelled
await agent.close();
Run the examples
llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M
llmman serve
cd demo.acp.sdk/nodejs-sdk
npm install # the SDK, and zod which it requires
node complete.js # not streamed
node stream.js # streamed, with the tool calls
node cancel.js # cancelled after 3 seconds
node permission.js # a command refused, then allowed
Node 20 or later. mm must be on the PATH, or named by MM_BIN. The examples use agent.yaml (Qwen3.5 4B on llmman, bash only, yolo: false); any agent file works.
To use the library from another project, until it is published: npm install ../path/to/demo.acp.sdk/nodejs-sdk, then import { Agent } from "minime-sdk";.
The API
await Agent.start({ config, cwd?, mm?, onPermission?, onStderr? }) |
starts mm -acp -config <config> (made absolute), then initialize and session/new {cwd}. mm defaults to $MM_BIN, then mm. |
await agent.complete(prompt, { signal?, onEvent? }) |
one turn; resolves to { text, stopReason, usage } once it is over. onEvent sees every event as it comes. |
agent.stream(prompt, { signal? }) |
one turn as an async iterator of events; the last one is done. Leaving the loop early cancels the turn. |
signal (an AbortSignal) |
aborting it sends session/cancel; the turn ends with stopReason: "cancelled" and the text so far. |
onPermission(call) |
asked when the agent wants to run a tool that needs a confirmation (yolo: false). Return true (allow once), false (reject once) or an ACP optionId ("allow_always", "never"). Without it, every such call is refused. A callback that throws refuses. |
onStderr(line) |
mm's stderr (banner, warnings, [acp] lines). Otherwise it is read and dropped; its last line is added to errors. |
await agent.reset() |
a new session on the same process: the conversation starts over. |
await agent.close() |
closes mm's stdin and waits for it to exit (kills it after 5 s). Calling it twice is harmless. |
agent.sessionId |
the ACP session id. |
eventOf(update), pickOption(options, choice) |
the two pure helpers, exported for tests and for anyone handling ACP messages themselves. |
The events: {kind: "text", text}, {kind: "thinking", text}, {kind: "tool_call", id, title, toolKind, input}, {kind: "tool_result", id, status, output}, {kind: "done", stopReason, usage, text}.
Rules worth knowing:
- One Agent = one mm process = one conversation. Calls continue it, as in an editor. One turn at a time per Agent (a second one throws
already running); start several Agents to work in parallel. - A turn mm ends with an error rejects with mm's own words and its last stderr line, e.g.
minime: Internal error (-32603): the model repeated the same bash call 6 times…. If mm crashed, the message saysmm exited: exit status N. - A cancelled question is dropped from the conversation by mm: the next question does not know about it.
- The permission callback can run before the
tool_callevent is read. The SDK answers mm's requests as soon as they arrive, while the session's updates wait in a queue until your loop reads them:stream.jsprints 🔐 before 🛠️. The dependency-free client reads both in arrival order. - The client advertises no file-system, terminal or form capability: mm needs none, and its
ask_usertool is not offered. The SDK answers any such request from mm with "method not found".
How it differs from ../nodejs/
nodejs/ |
nodejs-sdk/ |
|
|---|---|---|
| Protocol | JSON-RPC written by hand (~300 lines) | the official SDK: client(), ndJsonStream, buildSession(cwd).start(), ActiveSession.nextUpdate() |
| Dependencies | none | @agentclientprotocol/sdk 2.2.0 and zod (its peer dependency) |
| Message validation | none | the SDK checks the messages against the ACP schema |
| API | Agent.start, complete, stream, reset, close |
the same |
Tests
npm test # node --test test/*.test.js — 16 tests, no model, no mm, ~1 s
test/fake-mm.js plays mm's side of ACP as a real child process. It writes and reads the JSON lines itself, with the message shapes mm v1.0.3 sends (four permission options, rawOutput.output, usage on the prompt's answer, available_commands_update after session/new): answers, a thought, a tool call with its permission, a slow turn to cancel, an error, a crash.
Measured (2026-11-01, llmman on the author's host, mm v1.0.3 built from the working tree, Node 22)
| Example | Result |
|---|---|
complete.js |
one-sentence answer, end_turn, 663 tokens, 7.3 s |
stream.js |
find test -name "*.js" | wc -l allowed by the callback (read-only), output 2, answer streamed chunk by chunk, end_turn |
cancel.js |
story cut after 3.0 s (cancelled, 366 characters kept); the next question on the same Agent answered at once, without knowing the story, which mm dropped |
permission.js |
pwd refused: the example prints nothing (the model invented /root, 3 runs out of 3); allowed: the real directory |
Not done
- Not a published npm package: the
mmbinary would have to ship with it, per platform. session/load/session/resume(stored sessions), images and file attachments in prompts,ask_userforms: not exposed.- No timeout on a cancel mm never answers: the turn waits for mm's
cancelledanswer, as ACP requires.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
|