bots-garden/mini-mepublic Fork
⑂ feature/acp-client-tools
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. bfca5aa · on feature/acp-client-tools · k33g · 3d ago
README.en.md · 144 lines · 8.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
go run ./examples/clienttools   # a to-do list in this program, offered to the agent as two tools

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.Tools []Tool Go functions offered to the agent as tools (mm's ACP client tools, acp.clientTools, on by default). The model sees client_<Name>; each call runs Handle. Start refuses a tool with no Name, no Handle, a duplicate name, or a Method that does not start with _.
Options.ToolDiscovery minime.DiscoveryMeta (default: the tools travel in initialize) or minime.DiscoveryRequest (only true travels; the list is answered to _org.mini-me/tools/list). Must match acp.clientTools.discovery in the agent file.
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. Its only capability is _meta["org.mini-me/tools"], when Options.Tools has some.

Tool, an entry of Options.Tools:

Field
Name required, unique; the model sees client_<Name>.
Description what the model reads to decide when to call it.
Method the ACP extension method mm calls; must start with _. Default: _minime.go/tools/<Name>.
InputSchema map[string]any JSON schema of the arguments (type: object); optional.
Handle func(ctx, ToolCallRequest) (string, error) required. ToolCallRequest{SessionID, Arguments}. The text goes back to the model; an error (or a panic) goes back as a failed call (isError: true) and the client lives on. Calls may run concurrently.

With yolo: false in the agent file, each call is first submitted to OnPermission, titled client: <Name>.

Tests

cd demo.acp.sdk/go
go test ./...         # 47 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; and, like mm, it learns the client tools (from initialize, or with _org.mini-me/tools/list at session/new) and calls one. 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.

Added on 2026-11-10 (same host and model, mm built from branch feature/acp-client-tools):

Example Result
clienttools client_add_todo called twice (buy milk, write the blog post), client_list_todos twice; the answer lists both items, and the program's own list holds them. Same result with ToolDiscovery: DiscoveryRequest and acp.clientTools.discovery: request.

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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# 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
go run ./examples/clienttools   # a to-do list in this program, offered to the agent as two tools
```

`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.Tools []Tool` | Go functions offered to the agent as tools (mm's [ACP client tools](../../docs/en/reference/acp-client-tools.md), `acp.clientTools`, on by default). The model sees `client_<Name>`; each call runs `Handle`. `Start` refuses a tool with no `Name`, no `Handle`, a duplicate name, or a `Method` that does not start with `_`. |
| `Options.ToolDiscovery` | `minime.DiscoveryMeta` (default: the tools travel in `initialize`) or `minime.DiscoveryRequest` (only `true` travels; the list is answered to `_org.mini-me/tools/list`). Must match `acp.clientTools.discovery` in the agent file. |
| `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. Its only capability is `_meta["org.mini-me/tools"]`, when `Options.Tools` has some.

`Tool`, an entry of `Options.Tools`:

| Field | |
|---|---|
| `Name` | required, unique; the model sees `client_<Name>`. |
| `Description` | what the model reads to decide when to call it. |
| `Method` | the ACP extension method mm calls; must start with `_`. Default: `_minime.go/tools/<Name>`. |
| `InputSchema map[string]any` | JSON schema of the arguments (`type: object`); optional. |
| `Handle func(ctx, ToolCallRequest) (string, error)` | required. `ToolCallRequest{SessionID, Arguments}`. The text goes back to the model; an error (or a panic) goes back as a failed call (`isError: true`) and the client lives on. Calls may run concurrently. |

With `yolo: false` in the agent file, each call is first submitted to `OnPermission`, titled `client: <Name>`.

## Tests

```bash
cd demo.acp.sdk/go
go test ./...         # 47 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; and, like mm, it learns the client tools (from `initialize`, or with `_org.mini-me/tools/list` at `session/new`) and calls one. `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. |

Added on 2026-11-10 (same host and model, mm built from branch `feature/acp-client-tools`):

| Example | Result |
|---|---|
| `clienttools` | `client_add_todo` called twice (`buy milk`, `write the blog post`), `client_list_todos` twice; the answer lists both items, and the program's own list holds them. Same result with `ToolDiscovery: DiscoveryRequest` and `acp.clientTools.discovery: request`. |

## 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.