bots-garden/mini-mepublic Fork
⑂ v1.0.5
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.5 · k33g · 7d ago
README.fr.md · 121 lines · 8.3 KBmarkdown
Blame HistoryOpen raw

mini-me depuis Kotlin : minime.Agent

English version

Un client Kotlin (JVM) qui utilise mini-me comme bibliothèque en pilotant mm -acp par ACP (ticket 187). C'est le jumeau Kotlin des clients Node.js et Go. Il repose sur le SDK ACP officiel pour Kotlin, com.agentclientprotocol:acp 0.34.0 (Maven Central), et sur les coroutines. Voir ../README.fr.md pour les messages ACP qui se cachent derrière.

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

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

    launch { delay(3.seconds); agent.cancel() }                  // annulé au bout de 3 s
    agent.complete("A long task")                                // stopReason == "cancelled"
}

Lancer les exemples

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

cd demo.acp.sdk/kotlin
./gradlew runComplete      # sans stream
./gradlew runStream        # en stream, avec les appels d'outils
./gradlew runCancel        # annulé au bout de 3 secondes
./gradlew runPermission    # une commande refusée, puis autorisée

Il suffit d'un JDK (21 ou plus récent) : le wrapper Gradle télécharge Gradle, Kotlin et les dépendances au premier lancement. mm doit être dans le PATH, ou désigné par MM_BIN. Les exemples utilisent agent.yaml (Qwen3.5 4B sur llmman, bash seulement, yolo: false) ; tout fichier d'agent fonctionne. Leurs sources sont dans src/examples/kotlin/.

Pour utiliser la bibliothèque depuis un autre projet Gradle, tant qu'elle n'est pas publiée, incluez ce build dans le settings.gradle.kts du projet :

includeBuild("../chemin/vers/demo.acp.sdk/kotlin")

et déclarez la dépendance dans build.gradle.kts :

dependencies {
    implementation("minime:minime:0.1.0")
    runtimeOnly("org.slf4j:slf4j-nop:2.0.20")   // ou n'importe quel backend SLF4J, pour les logs du SDK
}

L'API

Agent.start(Options(...)): Agent (suspend) lance mm -acp -config <config> (rendu absolu) dans cwd, puis initialize et session/new {cwd}, en 60 s maximum. Lève MinimeException si mm ne démarre pas ou ne répond pas.
Options.config le fichier d'agent. Obligatoire.
Options.cwd le répertoire de travail de la session, là où les outils s'exécutent. Par défaut : le répertoire courant.
Options.mm le binaire mm. Par défaut : $MM_BIN, puis mm dans le PATH.
Options.onPermission: suspend (ToolCall) -> Decision appelé quand l'agent veut lancer un outil qui demande une confirmation (yolo: false). Sans lui, chacun de ces appels est refusé.
Options.onStderr: (String) -> Unit le stderr de mm, ligne par ligne (bannière, avertissements, lignes [acp]). Sinon il est lu puis jeté ; sa dernière ligne est ajoutée aux erreurs.
agent.complete(prompt, onEvent = null): Result (suspend) un tour ; renvoie Result(text, stopReason, usage) une fois terminé. onEvent voit chaque événement au fil de l'eau.
agent.stream(prompt): Flow<Event> un tour, lancé quand le flow est collecté ; le dernier événement est Event.Done. Annuler la coroutine qui collecte, ou quitter le flow avant la fin (first(), take(n)…), annule le tour : mm reçoit session/cancel.
agent.cancel() (suspend) annule le tour en cours, s'il y en a un : il se termine avec stopReason = "cancelled" et le texte déjà reçu, renvoyés normalement par complete / stream.
agent.reset() (suspend) une nouvelle session sur le même processus : la conversation repart de zéro.
agent.close() ferme le stdin de mm et attend qu'il se termine (le tue au bout de 5 s). Agent est AutoCloseable : use { } le ferme. L'appeler deux fois est sans effet.
agent.sessionId l'identifiant de session ACP.
AgentBusyException levée par un tour ou un reset pendant qu'un autre tour tourne sur le même Agent.
MinimeException un tour que mm termine par une erreur, ou mm parti : le message reprend les mots de mm et la dernière ligne de son stderr.

Decision, la réponse de onPermission :

Decision.Allow exécuter l'outil cette fois (allow_once)
Decision.Reject le refuser cette fois (reject_once)
Decision.Option(id) une des options proposées par mm, par son id : "allow", "allow_always" (cet outil, pour la session), "reject", "never" (cet outil, pour la session). Un id que mm n'a pas proposé vaut Reject.

Un callback qui lève une exception vaut Reject.

Les événements (sealed interface Event) :

Événement Champs
Event.Text text : un morceau de la réponse
Event.Thinking text : un morceau de la réflexion d'un modèle qui « réfléchit »
Event.ToolCallStarted call: ToolCall(id, title, kind, input) — title par ex. bash: ls, kind execute/read/edit…, input les arguments en texte JSON
Event.ToolResult id, status (completed ou failed), output
Event.Done stopReason, usage (null quand mm n'en a pas envoyé), text : toute la réponse

Les règles à connaître :

  • Un Agent = un processus mm = une conversation. Chaque appel la continue, comme dans un éditeur. Un seul tour à la fois par Agent (AgentBusyException) ; lancez plusieurs Agents pour travailler en parallèle.
  • Un tour que mm termine par une erreur lève MinimeException, avec les mots de mm et la dernière ligne de son stderr, par exemple minime: turn failed: Internal error (-32603): the model repeated the same bash call 6 times…. Si mm a planté, le message dit mm exited: exit status N.
  • mm retire de la conversation une question annulée : la question suivante n'en sait rien.
  • cancel() renvoie un résultat ; annuler la coroutine lève une exception. agent.cancel() laisse le tour se terminer normalement avec stopReason = "cancelled". Annuler la coroutine qui collecte (withTimeout, job.cancel()) arrête aussi le tour de mm, mais suit les règles des coroutines : complete lève la CancellationException.
  • Le client n'annonce aucune capacité fichiers, terminal ou formulaire : mm n'en a pas besoin, et son outil ask_user n'est pas proposé.
  • Le SDK journalise via SLF4J : ajoutez le backend de votre choix (slf4j-nop le rend silencieux) ; sans backend, SLF4J affiche un court avertissement au démarrage, puis se tait.

Tests

cd demo.acp.sdk/kotlin
./gradlew test        # 20 tests, sans modèle ni mm, ~5 s une fois Gradle chaud

src/test/kotlin/minime/FakeMm.kt est un faux mm, lancé comme un vrai processus enfant avec le classpath des tests. Il écrit et lit lui-même les lignes JSON, avec les formats de messages de mm v1.0.3 (quatre options de permission, rawOutput.output, usage dans la réponse au prompt, available_commands_update après session/new) : des réponses, une réflexion, un appel d'outil avec sa permission, un tour lent à annuler, une erreur, un plantage.

Mesures (2026-11-01, llmman sur la machine de l'auteur, mm v1.0.3 compilé depuis l'arbre de travail, JDK 25)

Exemple Résultat
runComplete réponse en une phrase, end_turn, 663 tokens, 9,3 s (modèle à froid)
runStream find src/examples -name "*.kt" | wc -l autorisé par le callback (lecture seule), résultat 4, réponse streamée morceau par morceau, end_turn
runCancel histoire coupée au bout de 3,0 s (cancelled, 332 caractères gardés) ; la question suivante sur le même Agent a eu sa réponse aussitôt, sans connaître l'histoire, que mm a retirée
runPermission pwd refusé : le modèle a inventé une réponse (/root) ; autorisé : le vrai répertoire. Un refus n'empêche pas un petit modèle de deviner.

Pas fait

  • Pas publié sur Maven Central : il faudrait livrer le binaire mm avec, pour chaque plateforme.
  • session/load / session/resume (sessions enregistrées), images et fichiers joints dans les prompts, formulaires ask_user : non exposés.
  • Pas de délai maximum pour une annulation à laquelle mm ne répondrait jamais (le client Go en a un, 30 s) : entourez l'appel d'un withTimeout si besoin.
  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 depuis Kotlin : `minime.Agent`

*[English version](README.en.md)*

Un client Kotlin (JVM) qui utilise mini-me comme bibliothèque en pilotant `mm -acp` par ACP (ticket 187). C'est le jumeau Kotlin des clients [Node.js](../nodejs/README.fr.md) et [Go](../go/README.fr.md). Il repose sur le SDK ACP officiel pour Kotlin, `com.agentclientprotocol:acp` 0.34.0 (Maven Central), et sur les coroutines. Voir [`../README.fr.md`](../README.fr.md) pour les messages ACP qui se cachent derrière.

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

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

    launch { delay(3.seconds); agent.cancel() }                  // annulé au bout de 3 s
    agent.complete("A long task")                                // stopReason == "cancelled"
}
```

## Lancer les exemples

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

cd demo.acp.sdk/kotlin
./gradlew runComplete      # sans stream
./gradlew runStream        # en stream, avec les appels d'outils
./gradlew runCancel        # annulé au bout de 3 secondes
./gradlew runPermission    # une commande refusée, puis autorisée
```

Il suffit d'un JDK (21 ou plus récent) : le wrapper Gradle télécharge Gradle, Kotlin et les dépendances au premier lancement. `mm` doit être dans le PATH, ou désigné par `MM_BIN`. Les exemples utilisent `agent.yaml` (Qwen3.5 4B sur llmman, bash seulement, `yolo: false`) ; tout fichier d'agent fonctionne. Leurs sources sont dans `src/examples/kotlin/`.

Pour utiliser la bibliothèque depuis un autre projet Gradle, tant qu'elle n'est pas publiée, incluez ce build dans le `settings.gradle.kts` du projet :

```kotlin
includeBuild("../chemin/vers/demo.acp.sdk/kotlin")
```

et déclarez la dépendance dans `build.gradle.kts` :

```kotlin
dependencies {
    implementation("minime:minime:0.1.0")
    runtimeOnly("org.slf4j:slf4j-nop:2.0.20")   // ou n'importe quel backend SLF4J, pour les logs du SDK
}
```

## L'API

| | |
|---|---|
| `Agent.start(Options(...)): Agent` | (suspend) lance `mm -acp -config <config>` (rendu absolu) dans `cwd`, puis `initialize` et `session/new {cwd}`, en 60 s maximum. Lève `MinimeException` si mm ne démarre pas ou ne répond pas. |
| `Options.config` | le fichier d'agent. Obligatoire. |
| `Options.cwd` | le répertoire de travail de la session, là où les outils s'exécutent. Par défaut : le répertoire courant. |
| `Options.mm` | le binaire mm. Par défaut : `$MM_BIN`, puis `mm` dans le PATH. |
| `Options.onPermission: suspend (ToolCall) -> Decision` | appelé quand l'agent veut lancer un outil qui demande une confirmation (`yolo: false`). **Sans lui, chacun de ces appels est refusé.** |
| `Options.onStderr: (String) -> Unit` | le stderr de mm, ligne par ligne (bannière, avertissements, lignes `[acp]`). Sinon il est lu puis jeté ; sa dernière ligne est ajoutée aux erreurs. |
| `agent.complete(prompt, onEvent = null): Result` | (suspend) un tour ; renvoie `Result(text, stopReason, usage)` une fois terminé. `onEvent` voit chaque événement au fil de l'eau. |
| `agent.stream(prompt): Flow<Event>` | un tour, lancé quand le flow est collecté ; le dernier événement est `Event.Done`. Annuler la coroutine qui collecte, ou quitter le flow avant la fin (`first()`, `take(n)`…), annule le tour : mm reçoit `session/cancel`. |
| `agent.cancel()` | (suspend) annule le tour en cours, s'il y en a un : il se termine avec `stopReason = "cancelled"` et le texte déjà reçu, renvoyés normalement par `complete` / `stream`. |
| `agent.reset()` | (suspend) une nouvelle session sur le même processus : la conversation repart de zéro. |
| `agent.close()` | ferme le stdin de mm et attend qu'il se termine (le tue au bout de 5 s). `Agent` est `AutoCloseable` : `use { }` le ferme. L'appeler deux fois est sans effet. |
| `agent.sessionId` | l'identifiant de session ACP. |
| `AgentBusyException` | levée par un tour ou un `reset` pendant qu'un autre tour tourne sur le même Agent. |
| `MinimeException` | un tour que mm termine par une erreur, ou mm parti : le message reprend les mots de mm et la dernière ligne de son stderr. |

`Decision`, la réponse de `onPermission` :

| | |
|---|---|
| `Decision.Allow` | exécuter l'outil cette fois (`allow_once`) |
| `Decision.Reject` | le refuser cette fois (`reject_once`) |
| `Decision.Option(id)` | une des options proposées par mm, par son id : `"allow"`, `"allow_always"` (cet outil, pour la session), `"reject"`, `"never"` (cet outil, pour la session). Un id que mm n'a pas proposé vaut `Reject`. |

Un callback qui lève une exception vaut `Reject`.

Les événements (`sealed interface Event`) :

| Événement | Champs |
|---|---|
| `Event.Text` | `text` : un morceau de la réponse |
| `Event.Thinking` | `text` : un morceau de la réflexion d'un modèle qui « réfléchit » |
| `Event.ToolCallStarted` | `call: ToolCall(id, title, kind, input)` — `title` par ex. `bash: ls`, `kind` `execute`/`read`/`edit`…, `input` les arguments en texte JSON |
| `Event.ToolResult` | `id`, `status` (`completed` ou `failed`), `output` |
| `Event.Done` | `stopReason`, `usage` (null quand mm n'en a pas envoyé), `text` : toute la réponse |

Les règles à connaître :

- **Un Agent = un processus mm = une conversation.** Chaque appel la continue, comme dans un éditeur. Un seul tour à la fois par Agent (`AgentBusyException`) ; lancez plusieurs Agents pour travailler en parallèle.
- **Un tour que mm termine par une erreur lève `MinimeException`**, avec les mots de mm et la dernière ligne de son stderr, par exemple `minime: turn failed: Internal error (-32603): the model repeated the same bash call 6 times…`. Si mm a planté, le message dit `mm exited: exit status N`.
- **mm retire de la conversation une question annulée** : la question suivante n'en sait rien.
- **`cancel()` renvoie un résultat ; annuler la coroutine lève une exception.** `agent.cancel()` laisse le tour se terminer normalement avec `stopReason = "cancelled"`. Annuler la coroutine qui collecte (`withTimeout`, `job.cancel()`) arrête aussi le tour de mm, mais suit les règles des coroutines : `complete` lève la `CancellationException`.
- Le client n'annonce aucune capacité fichiers, terminal ou formulaire : mm n'en a pas besoin, et son outil `ask_user` n'est pas proposé.
- Le SDK journalise via SLF4J : ajoutez le backend de votre choix (`slf4j-nop` le rend silencieux) ; sans backend, SLF4J affiche un court avertissement au démarrage, puis se tait.

## Tests

```bash
cd demo.acp.sdk/kotlin
./gradlew test        # 20 tests, sans modèle ni mm, ~5 s une fois Gradle chaud
```

`src/test/kotlin/minime/FakeMm.kt` est un faux mm, lancé comme un vrai processus enfant avec le classpath des tests. Il écrit et lit lui-même les lignes JSON, avec les formats de messages de mm v1.0.3 (quatre options de permission, `rawOutput.output`, usage dans la réponse au prompt, `available_commands_update` après `session/new`) : des réponses, une réflexion, un appel d'outil avec sa permission, un tour lent à annuler, une erreur, un plantage.

## Mesures (2026-11-01, llmman sur la machine de l'auteur, mm v1.0.3 compilé depuis l'arbre de travail, JDK 25)

| Exemple | Résultat |
|---|---|
| `runComplete` | réponse en une phrase, `end_turn`, 663 tokens, **9,3 s** (modèle à froid) |
| `runStream` | `find src/examples -name "*.kt" \| wc -l` autorisé par le callback (lecture seule), résultat `4`, réponse streamée morceau par morceau, `end_turn` |
| `runCancel` | histoire coupée au bout de **3,0 s** (`cancelled`, 332 caractères gardés) ; la question suivante sur le même Agent a eu sa réponse aussitôt, sans connaître l'histoire, que mm a retirée |
| `runPermission` | `pwd` refusé : le modèle a **inventé** une réponse (`/root`) ; autorisé : le vrai répertoire. Un refus n'empêche pas un petit modèle de deviner. |

## Pas fait

- Pas publié sur Maven Central : il faudrait livrer le binaire `mm` avec, pour chaque plateforme.
- `session/load` / `session/resume` (sessions enregistrées), images et fichiers joints dans les prompts, formulaires `ask_user` : non exposés.
- Pas de délai maximum pour une annulation à laquelle mm ne répondrait jamais (le client Go en a un, 30 s) : entourez l'appel d'un `withTimeout` si besoin.