bots-garden/mini-mepublic Fork
⑂ main
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 main · k33g · 7d ago
README.en.md · 121 lines · 7.6 KBmarkdown
Blame HistoryOpen raw

mini-me from Kotlin: minime.Agent

Version française

A Kotlin (JVM) client that uses mini-me as a library by driving mm -acp over ACP (ticket 187). It is the Kotlin twin of the Node.js and Go clients. It is built on the official ACP Kotlin SDK, com.agentclientprotocol:acp 0.34.0 (Maven Central), and on coroutines. See ../README.en.md for the ACP messages behind it.

Agent.start(Options(config = "agent.yaml")).use { agent ->
    val result = agent.complete("Explain this repository")      // not streamed
    println(result.text)

    agent.stream("And the tests?").collect { event ->           // streamed
        if (event is Event.Text) print(event.text)
    }

    launch { delay(3.seconds); agent.cancel() }                  // cancelled after 3 s
    agent.complete("A long task")                                // stopReason == "cancelled"
}

Run the examples

llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M
llmman serve

cd demo.acp.sdk/kotlin
./gradlew runComplete      # not streamed
./gradlew runStream        # streamed, with the tool calls
./gradlew runCancel        # cancelled after 3 seconds
./gradlew runPermission    # a command refused, then allowed

Only a JDK is needed (21 or later): the Gradle wrapper downloads Gradle, Kotlin and the dependencies on the first run. 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. Their sources are in src/examples/kotlin/.

To use the library from another Gradle project, until it is published, include this build in the project's settings.gradle.kts:

includeBuild("../path/to/demo.acp.sdk/kotlin")

and depend on it in build.gradle.kts:

dependencies {
    implementation("minime:minime:0.1.0")
    runtimeOnly("org.slf4j:slf4j-nop:2.0.20")   // or any SLF4J backend, for the SDK's logs
}

The API

Agent.start(Options(...)): Agent (suspend) starts mm -acp -config <config> (made absolute) in cwd, then initialize and session/new {cwd}, within 60 s. Throws MinimeException when mm cannot be started or does not answer.
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: suspend (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: (String) -> Unit 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(prompt, onEvent = null): Result (suspend) one turn; returns Result(text, stopReason, usage) once it is over. onEvent sees every event as it comes.
agent.stream(prompt): Flow<Event> one turn, run when the flow is collected; the last event is Event.Done. Cancelling the collecting coroutine, or leaving the flow early (first(), take(n)…), cancels the turn: mm receives session/cancel.
agent.cancel() (suspend) cancels the turn in progress, if any: it ends with stopReason = "cancelled" and the text so far, returned as usual by complete / stream.
agent.reset() (suspend) a new session on the same process: the conversation starts over.
agent.close() closes mm's stdin, waits for it to exit (kills it after 5 s). Agent is AutoCloseable: use { } closes it. Calling it twice is harmless.
agent.sessionId the ACP session id.
AgentBusyException thrown by a turn or a reset while another turn runs on the same Agent.
MinimeException a turn mm ended with an error, or mm gone: the message carries mm's words and its last stderr line.

Decision, the answer of onPermission:

Decision.Allow run the tool this once (allow_once)
Decision.Reject refuse it this once (reject_once)
Decision.Option(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 callback that throws counts as Reject.

The events (sealed interface Event):

Event Fields
Event.Text text: a chunk of the answer
Event.Thinking text: a chunk of a thinking model's reasoning
Event.ToolCallStarted call: ToolCall(id, title, kind, input) — title e.g. bash: ls, kind execute/read/edit…, input the arguments as JSON text
Event.ToolResult id, status (completed or failed), output
Event.Done stopReason, usage (null 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 (AgentBusyException); start several Agents to work in parallel.
  • A turn mm ends with an error throws MinimeException 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 message says mm exited: exit status N.
  • A cancelled question is dropped from the conversation by mm: the next question does not know about it.
  • cancel() returns a result; cancelling the coroutine throws. agent.cancel() lets the turn end normally with stopReason = "cancelled". Cancelling the collecting coroutine (withTimeout, job.cancel()) also stops mm's turn, but follows the coroutine rules: complete throws the CancellationException.
  • The client advertises no file-system, terminal or form capability: mm needs none, and its ask_user tool is not offered.
  • The SDK logs through SLF4J: add the backend you want (slf4j-nop keeps it silent); without one, SLF4J prints a short warning at start-up and then stays silent.

Tests

cd demo.acp.sdk/kotlin
./gradlew test        # 20 tests, no model, no mm, ~5 s once Gradle is warm

src/test/kotlin/minime/FakeMm.kt is a fake mm, started as a real child process on the test classpath. 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, JDK 25)

Example Result
runComplete one-sentence answer, end_turn, 663 tokens, 9.3 s (cold model)
runStream find src/examples -name "*.kt" | wc -l allowed by the callback (read-only), output 4, answer streamed chunk by chunk, end_turn
runCancel story cut after 3.0 s (cancelled, 332 characters kept); the next question on the same Agent answered at once, without knowing the story, which mm dropped
runPermission pwd refused: the model invented an answer (/root); allowed: the real directory. A refusal does not stop a small model from guessing.

Not done

  • Not published to Maven Central: 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.
  • No timeout on a cancel mm never answers (the Go client has one, 30 s): wrap the call in withTimeout if you need one.
  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
# mini-me from Kotlin: `minime.Agent`

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

A Kotlin (JVM) client that uses mini-me as a library by driving `mm -acp` over ACP (ticket 187). It is the Kotlin twin of the [Node.js](../nodejs/README.en.md) and [Go](../go/README.en.md) clients. It is built on the official ACP Kotlin SDK, `com.agentclientprotocol:acp` 0.34.0 (Maven Central), and on coroutines. See [`../README.en.md`](../README.en.md) for the ACP messages behind it.

```kotlin
Agent.start(Options(config = "agent.yaml")).use { agent ->
    val result = agent.complete("Explain this repository")      // not streamed
    println(result.text)

    agent.stream("And the tests?").collect { event ->           // streamed
        if (event is Event.Text) print(event.text)
    }

    launch { delay(3.seconds); agent.cancel() }                  // cancelled after 3 s
    agent.complete("A long task")                                // stopReason == "cancelled"
}
```

## Run the examples

```bash
llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M
llmman serve

cd demo.acp.sdk/kotlin
./gradlew runComplete      # not streamed
./gradlew runStream        # streamed, with the tool calls
./gradlew runCancel        # cancelled after 3 seconds
./gradlew runPermission    # a command refused, then allowed
```

Only a JDK is needed (21 or later): the Gradle wrapper downloads Gradle, Kotlin and the dependencies on the first run. `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. Their sources are in `src/examples/kotlin/`.

To use the library from another Gradle project, until it is published, include this build in the project's `settings.gradle.kts`:

```kotlin
includeBuild("../path/to/demo.acp.sdk/kotlin")
```

and depend on it in `build.gradle.kts`:

```kotlin
dependencies {
    implementation("minime:minime:0.1.0")
    runtimeOnly("org.slf4j:slf4j-nop:2.0.20")   // or any SLF4J backend, for the SDK's logs
}
```

## The API

| | |
|---|---|
| `Agent.start(Options(...)): Agent` | (suspend) starts `mm -acp -config <config>` (made absolute) in `cwd`, then `initialize` and `session/new {cwd}`, within 60 s. Throws `MinimeException` when mm cannot be started or does not answer. |
| `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: suspend (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: (String) -> Unit` | 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(prompt, onEvent = null): Result` | (suspend) one turn; returns `Result(text, stopReason, usage)` once it is over. `onEvent` sees every event as it comes. |
| `agent.stream(prompt): Flow<Event>` | one turn, run when the flow is collected; the last event is `Event.Done`. Cancelling the collecting coroutine, or leaving the flow early (`first()`, `take(n)`…), cancels the turn: mm receives `session/cancel`. |
| `agent.cancel()` | (suspend) cancels the turn in progress, if any: it ends with `stopReason = "cancelled"` and the text so far, returned as usual by `complete` / `stream`. |
| `agent.reset()` | (suspend) a new session on the same process: the conversation starts over. |
| `agent.close()` | closes mm's stdin, waits for it to exit (kills it after 5 s). `Agent` is `AutoCloseable`: `use { }` closes it. Calling it twice is harmless. |
| `agent.sessionId` | the ACP session id. |
| `AgentBusyException` | thrown by a turn or a `reset` while another turn runs on the same Agent. |
| `MinimeException` | a turn mm ended with an error, or mm gone: the message carries mm's words and its last stderr line. |

`Decision`, the answer of `onPermission`:

| | |
|---|---|
| `Decision.Allow` | run the tool this once (`allow_once`) |
| `Decision.Reject` | refuse it this once (`reject_once`) |
| `Decision.Option(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 callback that throws counts as `Reject`.

The events (`sealed interface Event`):

| Event | Fields |
|---|---|
| `Event.Text` | `text`: a chunk of the answer |
| `Event.Thinking` | `text`: a chunk of a thinking model's reasoning |
| `Event.ToolCallStarted` | `call: ToolCall(id, title, kind, input)` — `title` e.g. `bash: ls`, `kind` `execute`/`read`/`edit`…, `input` the arguments as JSON text |
| `Event.ToolResult` | `id`, `status` (`completed` or `failed`), `output` |
| `Event.Done` | `stopReason`, `usage` (null 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 (`AgentBusyException`); start several Agents to work in parallel.
- **A turn mm ends with an error throws `MinimeException`** 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 message says `mm exited: exit status N`.
- **A cancelled question is dropped from the conversation by mm**: the next question does not know about it.
- **`cancel()` returns a result; cancelling the coroutine throws.** `agent.cancel()` lets the turn end normally with `stopReason = "cancelled"`. Cancelling the collecting coroutine (`withTimeout`, `job.cancel()`) also stops mm's turn, but follows the coroutine rules: `complete` throws the `CancellationException`.
- The client advertises no file-system, terminal or form capability: mm needs none, and its `ask_user` tool is not offered.
- The SDK logs through SLF4J: add the backend you want (`slf4j-nop` keeps it silent); without one, SLF4J prints a short warning at start-up and then stays silent.

## Tests

```bash
cd demo.acp.sdk/kotlin
./gradlew test        # 20 tests, no model, no mm, ~5 s once Gradle is warm
```

`src/test/kotlin/minime/FakeMm.kt` is a fake mm, started as a real child process on the test classpath. 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, JDK 25)

| Example | Result |
|---|---|
| `runComplete` | one-sentence answer, `end_turn`, 663 tokens, **9.3 s** (cold model) |
| `runStream` | `find src/examples -name "*.kt" \| wc -l` allowed by the callback (read-only), output `4`, answer streamed chunk by chunk, `end_turn` |
| `runCancel` | story cut after **3.0 s** (`cancelled`, 332 characters kept); the next question on the same Agent answered at once, without knowing the story, which mm dropped |
| `runPermission` | `pwd` refused: the model **invented** an answer (`/root`); allowed: the real directory. A refusal does not stop a small model from guessing. |

## Not done

- Not published to Maven Central: 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.
- No timeout on a cancel mm never answers (the Go client has one, 30 s): wrap the call in `withTimeout` if you need one.