feat(stt): Phase 3 — actions admin pilotées par le LLM (admin_action) (#51)

Asa peut désormais AGIR sur le homelab quand on le demande explicitement, via
un outil de la boucle agentique — mais jamais sans confirmation.

- Outil admin_action(description) (contexte asa) : le LLM PROPOSE une action,
  n'exécute rien. brain.ask_with_tools gagne `confirm_tools` : un tel outil
  arrête la boucle et surface sa réponse (la question de confirmation).
- _handle_agentic : stocke la proposition en pending par session ; au tour
  suivant « confirme » → agent.run_action → hermes-exec (hermes -z --yolo),
  « annule » → oubli. Réutilise le handshake + jeton du contexte agent.
- admin_action n'est exposé que si _actions_available() (STT_ACTIONS_ENABLED
  + jeton) ; sinon retiré des schémas envoyés au modèle.
- Factorisation du ctx_debug du visualiseur. 1 test unitaire (confirm_tools
  arrête la boucle). Serveur 0.9.0 ; doc stt.md + journal.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ALI YESILKAYA 2026-06-22 22:51:11 +02:00 committed by GitHub
parent dc7a2093ae
commit 3ea4c3c706
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 163 additions and 28 deletions

View file

@ -109,12 +109,17 @@ async def ask_with_tools(
dispatch: Dispatch,
model: str | None = None,
history: list[dict] | None = None,
confirm_tools: tuple[str, ...] = (),
) -> tuple[str, list[dict]]:
"""Boucle de function-calling : le modèle appelle des outils jusqu'à pouvoir répondre.
Renvoie (réponse, trace) `trace` = [{name, args, result}] des outils exécutés
(alimente le visualiseur du HUD). Borné par `tool_max_iters` ; au-delà, un dernier
appel SANS outils force une réponse texte. Si aucun schéma n'est fourni, repli sur `ask`.
`confirm_tools` : outils « à confirmation » (ex. admin_action). Quand le modèle en appelle
un, la boucle s'ARRÊTE et renvoie directement le texte de son dispatch (la question de
confirmation) comme réponse l'exécution réelle est différée au tour suivant (app.py).
"""
if not schemas:
return await ask(text, system, model, history), []
@ -153,6 +158,9 @@ async def ask_with_tools(
args = {}
result = await dispatch(name, args)
trace.append({"name": name, "args": args, "result": result})
if name in confirm_tools:
# Outil à confirmation : on stoppe la boucle et on surface la question.
return result, trace
messages.append({
"role": "tool",
"tool_call_id": tc.get("id", ""),