mini-me depuis Kotlin : minime.Agent
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 exempleminime: turn failed: Internal error (-32603): the model repeated the same bash call 6 times…. Si mm a planté, le message ditmm 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 avecstopReason = "cancelled". Annuler la coroutine qui collecte (withTimeout,job.cancel()) arrête aussi le tour de mm, mais suit les règles des coroutines :completelève laCancellationException.- Le client n'annonce aucune capacité fichiers, terminal ou formulaire : mm n'en a pas besoin, et son outil
ask_usern'est pas proposé. - Le SDK journalise via SLF4J : ajoutez le backend de votre choix (
slf4j-nople 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
mmavec, pour chaque plateforme. session/load/session/resume(sessions enregistrées), images et fichiers joints dans les prompts, formulairesask_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
withTimeoutsi 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 |
|