mini-me from Go: package minime
A Go client that uses mini-me as a library by driving mm -acp over ACP (ticket 168). It is the Go twin of the Node.js client. It is its own module (minime), built on the ACP SDK mm itself uses, github.com/coder/acp-go-sdk v0.13.5, which has no dependency of its own. See ../README.en.md for the ACP messages behind it.
agent, err := minime.Start(ctx, minime.Options{Config: "agent.yaml"})
if err != nil {
log.Fatal(err)
}
defer agent.Close()
res, err := agent.Complete(ctx, "Explain this repository") // not streamed
for ev, err := range agent.Stream(ctx, "And the tests?") { // streamed
if err != nil {
log.Fatal(err)
}
if ev.Kind == minime.KindText {
fmt.Print(ev.Text)
}
}
ctx, cancel := context.WithTimeout(ctx, 3*time.Second) // cancelled after 3 s
defer cancel()
res, err = agent.Complete(ctx, "A long task") // res.StopReason == "cancelled"
Run the examples
llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M
llmman serve
cd demo.acp.sdk/go
go run ./examples/complete # not streamed
go run ./examples/stream # streamed, with the tool calls
go run ./examples/cancel # cancelled after 3 seconds
go run ./examples/permission # a command refused, then allowed
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. Go 1.24 or later (range-over-func).
To use the package from another module, until it is published:
go mod edit -require=minime@v0.0.0 -replace=minime=../path/to/demo.acp.sdk/go
go mod tidy
The API
minime.Start(ctx, minime.Options{...}) (*Agent, error) |
starts mm -acp -config <Config> (made absolute) in Cwd, then initialize and session/new {cwd}, within 60 s. |
Options.Config |
the agent file. Required. |
Options.Cwd |
the session's working directory, where the tools run. Default: the current directory. |
Options.MM |
the mm binary. Default: $MM_BIN, then mm on the PATH. |
Options.OnPermission func(ctx, ToolCall) Decision |
asked when the agent wants to run a tool that needs a confirmation (yolo: false). Without it, every such call is refused. |
Options.OnStderr func(line string) |
mm's stderr, line by line (banner, warnings, [acp] lines). Otherwise it is read and dropped; its last line is added to errors. |
agent.Complete(ctx, prompt) (Result, error) |
one turn; returns Result{Text, StopReason, Usage} once it is over. |
agent.Stream(ctx, prompt) iter.Seq2[Event, error] |
one turn as a range-over-func sequence; the last event is KindDone. Leaving the loop early cancels the turn. |
cancelling ctx |
sends session/cancel; the turn ends with StopReason: "cancelled", the text so far and a nil error. If mm does not answer within 30 s, the turn ends with an error. |
agent.Reset(ctx) error |
a new session on the same process: the conversation starts over. |
agent.Close() error |
closes mm's stdin, waits for it to exit (kills it after 5 s). Calling it twice is harmless. |
agent.SessionID() string |
the ACP session id. |
minime.ErrBusy |
returned by a turn or a Reset while another turn runs on the same Agent. |
Decision, the answer of OnPermission:
minime.Allow |
run the tool this once (allow_once) |
minime.Reject, or the zero Decision{} |
refuse it this once (reject_once) |
minime.ChooseOption(id) |
one of the options mm offers by its id: "allow", "allow_always" (this tool, for the session), "reject", "never" (this tool, for the session). An id mm did not offer counts as Reject. |
A panicking callback counts as Reject. Its ctx is the turn's.
The events (Event.Kind):
| Kind | Fields set |
|---|---|
KindText |
Text: a chunk of the answer |
KindThinking |
Text: a chunk of a thinking model's reasoning |
KindToolCall |
Call: ID, Title (e.g. bash: ls), Kind (execute, read, edit…), Input (the arguments) |
KindToolResult |
Call.ID, Status (completed or failed), Output |
KindDone |
StopReason, Usage (nil when mm sent none), Text: the whole answer |
Rules worth knowing:
- One Agent = one mm process = one conversation. Calls continue it, as in an editor. One turn at a time per Agent (
ErrBusy); start several Agents to work in parallel. - A turn mm ends with an error returns it with mm's own words and the last line of its stderr, e.g.
minime: turn failed: Internal error (-32603): the model repeated the same bash call 6 times…. If mm crashed, the error saysmm exited: exit status N. - A cancelled question is dropped from the conversation by mm: the next question does not know about it.
- The client advertises no file-system, terminal or form capability: mm needs none, and its
ask_usertool is not offered.
Tests
cd demo.acp.sdk/go
go test ./... # 40 tests and subtests, no model, no mm, ~1 s
The test binary plays a fake mm (fakemm_test.go): started again with MINIME_FAKE_MM=1, it answers over ACP with mm's message shapes (four permission options, rawOutput.output, usage on the prompt's answer): answers, a thought, a tool call with its permission, a slow turn to cancel, a turn deaf to the cancel, an error, a crash. go test -race needs cgo and a C compiler.
Measured (2026-11-01, llmman on the author's host, mm v1.0.3 built from the working tree)
| Example | Result |
|---|---|
complete |
one-sentence answer, end_turn, 663 tokens, 8.3 s (cold model) |
stream |
find examples -name "*.go" | wc -l allowed by the callback (read-only), output 4, answer streamed chunk by chunk, end_turn |
cancel |
story cut after 3.0 s (cancelled, 394 characters kept); the next question on the same Agent answered at once, without knowing the story, which mm dropped |
permission |
pwd refused: the model invented an answer (/root); allowed: the real directory. A refusal does not stop a small model from guessing. |
When to choose this, and when not
- This client needs the
mmbinary, but works today with no change to mm, and isolates each agent in its own process (mm's global state is no problem). - Ticket 139 (an in-process SDK) would need no binary, but mm's code would have to become an importable library first.
Not done
- Not a published Go module: 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.
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 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
|