bots-garden/mini-mepublic Fork
⑂ v1.0.4
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
🛟 Updated. eca9ec1 · on v1.0.4 · k33g · 7d ago
README.en.md · 123 lines · 6.6 KBmarkdown
Blame HistoryOpen raw

mini-me from Go: package minime

Version française

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 says mm 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_user tool 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 mm binary, 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 mm binary would have to ship with it, per platform.
  • session/load / session/resume (stored sessions), images and file attachments in prompts, ask_user forms: 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
# mini-me from Go: package `minime`

*[Version française](README.fr.md)*

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](../nodejs/README.en.md). 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`](../README.en.md) for the ACP messages behind it.

```go
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

```bash
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:

```bash
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 says `mm 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_user` tool is not offered.

## Tests

```bash
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 `mm` binary, 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 `mm` binary would have to ship with it, per platform.
- `session/load` / `session/resume` (stored sessions), images and file attachments in prompts, `ask_user` forms: not exposed.