mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-21 22:18:13 +02:00
Tâches : scripts planifiés sans modèle, et l'IA planifie la sienne
Deux reprises d'AJEAN autour des tâches planifiées. Dossier de scripts (/data/scripts) : un dossier durable À CÔTÉ de memory, hors du workspace. Le workspace est jetable — supprimer une discussion emporte ses fichiers — donc un script qu'on veut garder n'y avait pas sa place. Le briefing machine l'annonce à l'IA, qui y écrit et y lance ses scripts normalement. Tâche « script seul » (Task.Kind/Script) : le planificateur lance le script sans charger le modèle ni consommer un token, sa sortie devient le compte-rendu, et l'UI l'affiche comme n'importe quelle tâche. Elle tourne hors du verrou de génération — d'où un registre à part pour l'afficher « en cours » et l'arrêter (/api/tasks/stop), et un « tester » qui n'attend ni le verrou ni le moteur. Sélecteur Consigne IA / Script seul dans la modale. Outils task_list/create/update/delete : l'IA se donne elle-même rappels et veilles récurrentes, cloisonnés par projet (dans un projet, elle ne voit et ne pilote que ses tâches). Le budget du préambule passe de 8600 à 11000 caractères, avec le palier documenté dans le test. Dossier mémoire réservé à ses outils : bash, write et edit ne le touchent plus (guardToolOnly*). Un `cat memory/…` contournait l'index MEMORY.md, et c'est la condition d'un chiffrement de la mémoire à venir. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
This commit is contained in:
17 files changed
+1043
-26
No files matched your search
@@ -21,6 +21,14 @@ func handleTasks(w http.ResponseWriter, r *http.Request) {
|
||||
conv.mu.Lock()
|
||||
runningID := conv.runningTaskID
|
||||
conv.mu.Unlock()
|
||||
// Une tâche script tourne HORS du verrou de génération (aucune inférence) :
|
||||
// conv ne la voit pas, on complète donc avec le registre des scripts en cours
|
||||
// (tasks_script.go), sinon l'UI l'affiche à l'arrêt pendant qu'elle tourne.
|
||||
if runningID == "" {
|
||||
if sid, _ := scriptRunningAny(); sid != "" {
|
||||
runningID = sid
|
||||
}
|
||||
}
|
||||
// Presets (id + nom + actif) pour peupler le sélecteur du formulaire de tâche.
|
||||
presets := []map[string]any{}
|
||||
if list, err := ListPresets(); err == nil {
|
||||
@@ -28,13 +36,20 @@ func handleTasks(w http.ResponseWriter, r *http.Request) {
|
||||
presets = append(presets, map[string]any{"id": p.ID, "name": p.Name, "active": p.Active})
|
||||
}
|
||||
}
|
||||
// Scripts durables disponibles, pour le sélecteur d'une tâche « script seul ».
|
||||
scripts, _ := listScripts()
|
||||
if scripts == nil {
|
||||
scripts = []scriptInfo{}
|
||||
}
|
||||
sendJSON(w, 200, map[string]any{
|
||||
"ok": true,
|
||||
"tasks": tasks,
|
||||
"paused": tasksPaused(),
|
||||
"agent": agentEnabled(),
|
||||
"running_id": runningID,
|
||||
"presets": presets,
|
||||
"ok": true,
|
||||
"tasks": tasks,
|
||||
"scripts": scripts,
|
||||
"scripts_dir": scriptsDir(),
|
||||
"paused": tasksPaused(),
|
||||
"agent": agentEnabled(),
|
||||
"running_id": runningID,
|
||||
"presets": presets,
|
||||
// Projets, pour le sélecteur « projet visé » du formulaire de tâche.
|
||||
"projects": projectDTOs(),
|
||||
// État global mémoire/web, pour proposer des défauts cohérents à la création.
|
||||
@@ -54,6 +69,8 @@ func handleTaskSave(w http.ResponseWriter, r *http.Request) {
|
||||
TZ string `json:"tz"`
|
||||
Preset string `json:"preset"`
|
||||
Project string `json:"project"`
|
||||
Kind string `json:"kind"`
|
||||
Script string `json:"script"`
|
||||
NoMem bool `json:"no_mem"`
|
||||
NoWeb bool `json:"no_web"`
|
||||
Enabled bool `json:"enabled"`
|
||||
@@ -65,8 +82,26 @@ func handleTaskSave(w http.ResponseWriter, r *http.Request) {
|
||||
req.Name = strings.TrimSpace(req.Name)
|
||||
req.Prompt = strings.TrimSpace(req.Prompt)
|
||||
req.Schedule = strings.TrimSpace(req.Schedule)
|
||||
if req.Name == "" || req.Prompt == "" {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": "nom et consigne obligatoires"})
|
||||
req.Kind = strings.TrimSpace(req.Kind)
|
||||
req.Script = strings.TrimSpace(req.Script)
|
||||
if req.Name == "" {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": "nom obligatoire"})
|
||||
return
|
||||
}
|
||||
// Une tâche script exige un script EXISTANT (le vérifier ici évite une tâche
|
||||
// planifiée qui échouera silencieusement au premier tic) ; une tâche IA exige
|
||||
// une consigne.
|
||||
if req.Kind == "script" {
|
||||
if req.Script == "" {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": "script obligatoire pour une tâche script"})
|
||||
return
|
||||
}
|
||||
if err := scriptExists(req.Script); err != nil {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
} else if req.Prompt == "" {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": "consigne obligatoire"})
|
||||
return
|
||||
}
|
||||
if err := validateSchedule(req.Schedule, req.TZ); err != nil {
|
||||
@@ -87,6 +122,7 @@ func handleTaskSave(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
t.Name, t.Prompt, t.Enabled = req.Name, req.Prompt, req.Enabled
|
||||
t.TZ, t.Preset = req.TZ, req.Preset
|
||||
t.Kind, t.Script = req.Kind, req.Script
|
||||
// Projet visé : vide = le projet actif au moment de l'enregistrement. Une tâche
|
||||
// créée depuis un projet appartient à ce projet — c'est là que sa mémoire et
|
||||
// ses trackers l'attendent quand elle tournera (voir runTask).
|
||||
@@ -162,6 +198,22 @@ func handleTasksPause(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, 200, map[string]any{"ok": true, "paused": tasksPaused()})
|
||||
}
|
||||
|
||||
// handleTaskStop arrête la tâche en cours. Une tâche SCRIPT ne passe pas par le
|
||||
// verrou de génération : on l'annule via son registre (tasks_script.go). Sinon
|
||||
// (tâche IA), on retombe sur conv.Stop, qui annule la génération autonome.
|
||||
func handleTaskStop(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
if req.ID != "" && scriptRunStop(req.ID) {
|
||||
sendJSON(w, 200, map[string]any{"ok": true})
|
||||
return
|
||||
}
|
||||
conv.Stop()
|
||||
sendJSON(w, 200, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
// handleTaskRun lance une tâche MAINTENANT (bouton « tester »). Exécution
|
||||
// détachée : on rend la main tout de suite et on rafraîchit l'état à la fin.
|
||||
// 409 si une génération est déjà en cours.
|
||||
@@ -175,6 +227,14 @@ func handleTaskRun(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, 404, map[string]any{"ok": false, "error": "tâche introuvable"})
|
||||
return
|
||||
}
|
||||
// Tâche script : aucune inférence, donc ni verrou de génération ni moteur
|
||||
// chargé à attendre — la refuser parce que le modèle répond ailleurs n'aurait
|
||||
// aucun sens.
|
||||
if t.Kind == "script" {
|
||||
go runTask(t)
|
||||
sendJSON(w, 200, map[string]any{"ok": true})
|
||||
return
|
||||
}
|
||||
// Vérifie la disponibilité AVANT de détacher, pour pouvoir répondre 409/503.
|
||||
conv.mu.Lock()
|
||||
busy := conv.Generating
|
||||
|
||||
Reference in new issue
Block a user