mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
v0.2.10 : mode agent unifie (un seul toggle = shell + skills) + jeu de mot 'ajean' + refonte du header (pastille d'etat, preset, espacements)
This commit is contained in:
1 parent
175bbd34d9
commit
d3a2f1c428
11 files changed
+199
-207
No files matched your search
@@ -21,7 +21,7 @@ Faire tourner llama.cpp comme un vrai service, ça veut dire d'habitude : trouve
|
||||
- **ROCm/HIP** (AMD), **Metal** (macOS / Apple Silicon), **Vulkan**, ou repli **CPU**
|
||||
- `update` récupère le dernier commit, arrête le service le temps de recompiler, puis le redémarre
|
||||
- **Intégration systemd** — `jean install` écrit l'unité, une règle sudoers `systemctl` sans mot de passe, et les dossiers de données
|
||||
- **Interface web** (`jean web`) — chat, changement de modèle/preset, activation des skills & tools
|
||||
- **Interface web** (`jean web`) — chat, changement de modèle/preset, activation du mode agent
|
||||
- **Chat terminal** (`jean chat`) — réponses en streaming
|
||||
- **Accès distant chiffré** (`jean link`) — connecte ce serveur à [ajean.link](https://ajean.link) par une connexion **sortante** (aucun port à ouvrir, marche même en CGNAT). Le chat est **chiffré de bout en bout** : le relais ne voit jamais tes conversations (voir [Sécurité — boîte noire](#sécurité--boîte-noire))
|
||||
- **Presets** (`jean switch`) — garde plusieurs profils `config.env` et bascule entre eux
|
||||
@@ -119,9 +119,8 @@ Accès distant (ajean.link) :
|
||||
link code génère un code d'appairage (valable 10 min, usage unique) pour le portail
|
||||
link status | logout état du lien / oublier le token
|
||||
|
||||
Outils côté LLM :
|
||||
skills [on|off|list] laisse le modèle lire SKILLS/<nom>/SKILL.md
|
||||
machine [on|off|status] active l'accès machine (le modèle dispose d'un shell complet sur le serveur)
|
||||
Mode agent :
|
||||
agent [on|off|status] active TOUS les outils du modèle (shell complet + skills) — un seul interrupteur
|
||||
|
||||
Backend (llama.cpp) :
|
||||
llamacpp install clone + compile llama.cpp (détecte CUDA/ROCm/Metal/CPU), règle BIN
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Le « mode agent » est l'unique interrupteur qui donne à l'IA l'accès à ses
|
||||
// outils. Un skill est un outil comme un autre : quand le mode agent est actif,
|
||||
// l'IA dispose à la fois du shell (run_shell) et de la gestion des skills.
|
||||
// Plus de drapeaux machine/skills séparés — un seul fichier .agent_enabled.
|
||||
|
||||
func agentEnabled() bool {
|
||||
if _, err := os.Stat(agentFlag()); err == nil {
|
||||
return true
|
||||
}
|
||||
// Migration : si l'un des anciens drapeaux séparés traîne encore, on
|
||||
// considère le mode agent comme actif (et on le matérialise au prochain set).
|
||||
if _, err := os.Stat(legacyToolsFlag()); err == nil {
|
||||
return true
|
||||
}
|
||||
if _, err := os.Stat(legacySkillsFlag()); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func setAgentEnabled(on bool) error {
|
||||
_ = os.MkdirAll(JeanHome(), 0o755)
|
||||
// On nettoie systématiquement les anciens drapeaux pour ne pas garder un
|
||||
// état fantôme « à moitié activé » hérité de l'ancien modèle à deux toggles.
|
||||
_ = os.Remove(legacyToolsFlag())
|
||||
_ = os.Remove(legacySkillsFlag())
|
||||
if on {
|
||||
f, err := os.Create(agentFlag())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
if err := os.Remove(agentFlag()); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdAgent(args []string) error {
|
||||
sub := ""
|
||||
if len(args) > 0 {
|
||||
sub = args[0]
|
||||
}
|
||||
switch sub {
|
||||
case "on":
|
||||
if err := setAgentEnabled(true); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(green("[ok]") + " mode agent activé — l'IA dispose du shell complet et de ses skills")
|
||||
case "off":
|
||||
if err := setAgentEnabled(false); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(green("[ok]") + " mode agent désactivé")
|
||||
case "", "status", "list":
|
||||
state := dim("off")
|
||||
if agentEnabled() {
|
||||
state = green("on")
|
||||
}
|
||||
fmt.Printf("%s état: %s\n", cyan("Mode agent"), state)
|
||||
fmt.Printf(" outils : run_shell (timeout %ds, max %ds) + skill (read/write/append/delete)\n", toolDefaultTimeout, toolMaxTimeout)
|
||||
sk := ListSkills()
|
||||
if len(sk) == 0 {
|
||||
fmt.Printf(" skills : aucun — crée %s/<nom>/SKILL.md\n", skillsDir())
|
||||
return nil
|
||||
}
|
||||
fmt.Printf(" skills (%s) :\n", skillsDir())
|
||||
for _, s := range sk {
|
||||
fmt.Printf(" %s %s\n", bold(s.Name), s.Desc)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("usage: jean agent [on|off|status]")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -87,14 +87,15 @@ func runShellTool() Tool {
|
||||
// what the model can do for this conversation, instead of always inheriting the
|
||||
// machine's global config. Use globalCaps() to fall back to the global config.
|
||||
type Caps struct {
|
||||
Tools bool
|
||||
Skills bool
|
||||
// Agent = mode agent actif : un seul interrupteur qui débloque TOUS les
|
||||
// outils de l'IA (shell + skills). Un skill est un outil comme un autre.
|
||||
Agent bool
|
||||
}
|
||||
|
||||
// globalCaps reads the machine-wide config — the default when a request doesn't
|
||||
// specify its own capabilities.
|
||||
func globalCaps() Caps {
|
||||
return Caps{Tools: toolsEnabled(), Skills: skillsEnabled()}
|
||||
return Caps{Agent: agentEnabled()}
|
||||
}
|
||||
|
||||
// InjectSkills prepends context system messages to msgs: the decisive-agent
|
||||
@@ -130,11 +131,8 @@ func InjectSkills(msgs []Message, caps Caps) []Message {
|
||||
// EnabledTools returns the tools to advertise on the next inference call.
|
||||
func EnabledTools(caps Caps) []Tool {
|
||||
tools := []Tool{}
|
||||
if caps.Skills {
|
||||
tools = append(tools, skillTool())
|
||||
}
|
||||
if caps.Tools {
|
||||
tools = append(tools, runShellTool())
|
||||
if caps.Agent {
|
||||
tools = append(tools, skillTool(), runShellTool())
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const Version = "0.2.9"
|
||||
const Version = "0.2.10"
|
||||
|
||||
func main() {
|
||||
args := os.Args[1:]
|
||||
@@ -47,10 +47,11 @@ func main() {
|
||||
mustExit(cmdWeb(args))
|
||||
case "link":
|
||||
mustExit(cmdLink(args))
|
||||
case "skills":
|
||||
mustExit(cmdSkills(args))
|
||||
case "machine", "tools":
|
||||
mustExit(cmdTools(args))
|
||||
case "agent", "skills", "machine", "tools":
|
||||
// « mode agent » unifie l'ancien couple machine + skills : un seul
|
||||
// interrupteur active TOUS les outils de l'IA (shell + skills). Les
|
||||
// anciens noms restent acceptés comme alias rétro-compatibles.
|
||||
mustExit(cmdAgent(args))
|
||||
case "serve":
|
||||
mustExit(cmdServe(args))
|
||||
case "test":
|
||||
@@ -96,7 +97,7 @@ Presets:
|
||||
|
||||
Interaction:
|
||||
chat [system-prompt] chat terminal streamé
|
||||
web [PORT] UI web (défaut :8090) — chat + presets + skills + tools
|
||||
web [PORT] UI web (défaut :8090) — chat + presets + mode agent
|
||||
|
||||
Accès distant (ajean.link) :
|
||||
link <token> enregistre le token et démarre le lien au relais (token = 1re fois / pour le changer)
|
||||
@@ -106,9 +107,8 @@ Accès distant (ajean.link) :
|
||||
link (sans argument) affiche l'aide des sous-commandes link
|
||||
link serve exécute le worker au premier plan (utilisé par jean-link.service ; pendant de 'jean serve')
|
||||
|
||||
LLM-side outils:
|
||||
skills [on|off|list] active la lecture de SKILLS/<nom>/SKILL.md par l'IA
|
||||
machine [on|off|status] active l'accès machine (l'IA dispose d'un shell complet sur le serveur)
|
||||
Mode agent:
|
||||
agent [on|off|status] active TOUS les outils de l'IA (shell complet + skills) — un seul interrupteur
|
||||
|
||||
Backend llama.cpp :
|
||||
llamacpp install clone + compile llama.cpp (détecte CUDA/ROCm/Metal/CPU), pointe BIN dessus
|
||||
@@ -176,8 +176,10 @@ func readEtcDefault() string {
|
||||
func confPath() string { return filepath.Join(JeanHome(), "config.env") }
|
||||
func presetsDir() string { return filepath.Join(JeanHome(), "configs") }
|
||||
func skillsDir() string { return filepath.Join(JeanHome(), "SKILLS") }
|
||||
func skillsFlag() string { return filepath.Join(skillsDir(), ".enabled") }
|
||||
func toolsFlag() string { return filepath.Join(JeanHome(), ".tools_enabled") }
|
||||
func agentFlag() string { return filepath.Join(JeanHome(), ".agent_enabled") }
|
||||
// Anciens drapeaux séparés, conservés pour la migration vers le mode agent unifié.
|
||||
func legacySkillsFlag() string { return filepath.Join(skillsDir(), ".enabled") }
|
||||
func legacyToolsFlag() string { return filepath.Join(JeanHome(), ".tools_enabled") }
|
||||
func apiKeyPath() string { return filepath.Join(JeanHome(), ".api_key") }
|
||||
func serviceName() string {
|
||||
if n := os.Getenv("JEAN_SERVICE"); n != "" {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -15,26 +15,6 @@ type Skill struct {
|
||||
Content string
|
||||
}
|
||||
|
||||
func skillsEnabled() bool {
|
||||
_, err := os.Stat(skillsFlag())
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func setSkillsEnabled(on bool) error {
|
||||
_ = os.MkdirAll(skillsDir(), 0o755)
|
||||
if on {
|
||||
f, err := os.Create(skillsFlag())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
if err := os.Remove(skillsFlag()); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListSkills walks SKILLS/<name>/SKILL.md, returning name+desc+full content.
|
||||
func ListSkills() []Skill {
|
||||
entries, err := os.ReadDir(skillsDir())
|
||||
@@ -157,7 +137,7 @@ func DeleteSkill(name string) error {
|
||||
// skillsSystemPrompt returns the lightweight skills directory message to
|
||||
// prepend to the conversation when skills are enabled.
|
||||
func skillsSystemPrompt(caps Caps) string {
|
||||
if !caps.Skills {
|
||||
if !caps.Agent {
|
||||
return ""
|
||||
}
|
||||
list := ListSkills()
|
||||
@@ -181,39 +161,3 @@ func skillsSystemPrompt(caps Caps) string {
|
||||
}
|
||||
return strings.TrimRight(b.String(), "\n")
|
||||
}
|
||||
|
||||
func cmdSkills(args []string) error {
|
||||
sub := ""
|
||||
if len(args) > 0 {
|
||||
sub = args[0]
|
||||
}
|
||||
switch sub {
|
||||
case "on":
|
||||
if err := setSkillsEnabled(true); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(green("[ok]") + " skills activés")
|
||||
case "off":
|
||||
if err := setSkillsEnabled(false); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(green("[ok]") + " skills désactivés")
|
||||
case "", "list":
|
||||
state := dim("off")
|
||||
if skillsEnabled() {
|
||||
state = green("on")
|
||||
}
|
||||
fmt.Printf("%s (%s) état: %s\n", cyan("Skills"), skillsDir(), state)
|
||||
sk := ListSkills()
|
||||
if len(sk) == 0 {
|
||||
fmt.Printf(" (aucun — crée %s/<nom>/SKILL.md)\n", skillsDir())
|
||||
return nil
|
||||
}
|
||||
for _, s := range sk {
|
||||
fmt.Printf(" %s %s\n", bold(s.Name), s.Desc)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("usage: jean skills [on|off|list]")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -17,26 +17,6 @@ const (
|
||||
toolMaxOutput = 8000 // characters of stdout/stderr returned to the model
|
||||
)
|
||||
|
||||
func toolsEnabled() bool {
|
||||
_, err := os.Stat(toolsFlag())
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func setToolsEnabled(on bool) error {
|
||||
_ = os.MkdirAll(JeanHome(), 0o755)
|
||||
if on {
|
||||
f, err := os.Create(toolsFlag())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
if err := os.Remove(toolsFlag()); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// baseSystemPrompt is the always-on system preamble. Structured like pi's
|
||||
// proven prompt (identity → method → guidelines → concision → date): a concrete,
|
||||
// procedural prompt gives the model rails so it stops deliberating forever
|
||||
@@ -47,7 +27,7 @@ func baseSystemPrompt(caps Caps) string {
|
||||
// No tool access → no agentic preamble. A plain chat model told to "call
|
||||
// tools immediately" hallucinates textual tool calls (e.g. default_api:bash)
|
||||
// that leak into the answer. Let the user's own system prompt stand alone.
|
||||
if !caps.Tools && !caps.Skills {
|
||||
if !caps.Agent {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
@@ -72,7 +52,7 @@ func baseSystemPrompt(caps Caps) string {
|
||||
// run_shell acts upon (and doesn't claim it has no access to "your PC").
|
||||
// Returns "" when machine access is off.
|
||||
func machineSystemPrompt(caps Caps) string {
|
||||
if !caps.Tools {
|
||||
if !caps.Agent {
|
||||
return ""
|
||||
}
|
||||
host, _ := os.Hostname()
|
||||
@@ -156,32 +136,3 @@ func tailRunes(s string, n int) string {
|
||||
}
|
||||
return string(r[len(r)-n:])
|
||||
}
|
||||
|
||||
func cmdTools(args []string) error {
|
||||
sub := ""
|
||||
if len(args) > 0 {
|
||||
sub = args[0]
|
||||
}
|
||||
switch sub {
|
||||
case "on":
|
||||
if err := setToolsEnabled(true); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(green("[ok]") + " accès machine activé — l'IA dispose d'un shell complet sur le serveur")
|
||||
case "off":
|
||||
if err := setToolsEnabled(false); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(green("[ok]") + " accès machine désactivé")
|
||||
case "", "status":
|
||||
state := dim("off")
|
||||
if toolsEnabled() {
|
||||
state = green("on")
|
||||
}
|
||||
fmt.Printf("%s état: %s\n", cyan("Accès machine"), state)
|
||||
fmt.Printf(" timeout défaut: %ds, max: %ds\n", toolDefaultTimeout, toolMaxTimeout)
|
||||
default:
|
||||
return fmt.Errorf("usage: jean machine [on|off|status]")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+62
-40
@@ -30,10 +30,34 @@ body{margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background
|
||||
.preset-meta .qtag,.preset-meta .rtag,.preset-meta .btag{font-size:11px;padding:2px 9px;border-radius:9px}
|
||||
.preset-meta .qtag svg,.preset-meta .rtag svg,.preset-meta .btag svg{width:11px;height:11px}
|
||||
}
|
||||
h1{margin:0 0 4px;font-size:18px;color:var(--accent)}
|
||||
h1{margin:0;font-size:18px;color:var(--accent)}
|
||||
#brand{display:inline-flex;align-items:baseline}
|
||||
.sidehead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:18px}
|
||||
/* pastille d'état service, en haut à droite de la barre latérale */
|
||||
.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap}
|
||||
.statuspill.ok{color:var(--ok);border-color:rgba(63,185,80,.45);background:rgba(63,185,80,.10)}
|
||||
.statuspill.err{color:var(--err);border-color:rgba(248,81,73,.45);background:rgba(248,81,73,.10)}
|
||||
.statuspill .dot{width:7px;height:7px;border-radius:50%;background:currentColor}
|
||||
.statuspill .port{opacity:.6;font-weight:400}
|
||||
/* en-tête + bouton d'ajout de skill (toujours visible) */
|
||||
.skillshead{display:flex;align-items:center;justify-content:space-between;margin:12px 0 5px}
|
||||
.skillshead>span{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.08em;font-weight:600}
|
||||
.addbtn{font-size:11px;padding:3px 11px 4px;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;cursor:pointer;line-height:1.2}
|
||||
.addbtn:hover{border-color:var(--accent);color:var(--accent)}
|
||||
#brand-a{display:inline-block;line-height:1;max-width:0;opacity:0;overflow:hidden;color:#fff;transition:max-width .35s ease,opacity .35s ease}
|
||||
#brand.agent #brand-a{max-width:1ch;opacity:1;margin-right:.04em}
|
||||
/* interrupteur (switch) */
|
||||
.switchrow{display:flex;align-items:center;gap:9px;font-size:12px;padding:6px 0;cursor:pointer;user-select:none}
|
||||
.switch{position:relative;display:inline-block;width:36px;height:20px;flex:none}
|
||||
.switch input{position:absolute;opacity:0;width:0;height:0}
|
||||
.switch .slider{position:absolute;inset:0;background:var(--border);border-radius:999px;transition:background .25s ease}
|
||||
.switch .slider::before{content:"";position:absolute;width:14px;height:14px;left:3px;top:3px;background:#fff;border-radius:50%;transition:transform .25s ease;box-shadow:0 1px 2px rgba(0,0,0,.4)}
|
||||
.switch input:checked + .slider{background:var(--accent)}
|
||||
.switch input:checked + .slider::before{transform:translateX(16px)}
|
||||
.switch input:focus-visible + .slider{box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--accent)}
|
||||
h2{margin:20px 0 8px;font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;font-weight:600}
|
||||
details{margin:8px 0;border-top:1px solid var(--border);padding-top:6px}
|
||||
details>summary{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;font-weight:600;cursor:pointer;padding:4px 0;list-style:none;display:flex;justify-content:space-between;align-items:center;user-select:none}
|
||||
details{margin:7px 0;border-top:1px solid var(--border);padding-top:4px}
|
||||
details>summary{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;font-weight:600;cursor:pointer;padding:3px 0;list-style:none;display:flex;justify-content:space-between;align-items:center;user-select:none}
|
||||
details>summary::-webkit-details-marker{display:none}
|
||||
details>summary::after{content:"▸";transition:transform .15s;color:var(--dim);font-size:10px}
|
||||
details[open]>summary::after{transform:rotate(90deg)}
|
||||
@@ -72,6 +96,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
.msg.assistant,.msg.reasoning{white-space:normal}
|
||||
.msg .body{overflow-wrap:anywhere;word-break:break-word}
|
||||
.msg.user{align-self:flex-end;background:#1f6feb;color:#fff}
|
||||
.msg.user .label{color:rgba(255,255,255,.8)}
|
||||
.msg.assistant{align-self:flex-start;background:var(--panel);border:1px solid var(--border)}
|
||||
.msg .body>*{margin:1em 0}
|
||||
.msg .body>*:first-child{margin-top:0}
|
||||
@@ -145,8 +170,10 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
<button id="menubtn" onclick="toggleSide()">☰</button>
|
||||
<div id="backdrop" onclick="toggleSide()"></div>
|
||||
<div class="side" id="side">
|
||||
<h1>jean</h1>
|
||||
<div id="statusline" class="muted"><span id="status-svc">…</span> <span id="status-preset"></span></div>
|
||||
<div class="sidehead">
|
||||
<h1 id="brand"><span id="brand-a">a</span>jean</h1>
|
||||
<span id="status-svc" class="statuspill">…</span>
|
||||
</div>
|
||||
<details open><summary>GPU</summary>
|
||||
<div id="vram" class="muted">…</div>
|
||||
</details>
|
||||
@@ -156,12 +183,10 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
<details><summary>Presets<button class="preset-add" onclick="event.preventDefault();event.stopPropagation();openPreset('')" title="ajouter">+</button></summary>
|
||||
<div id="presets"></div>
|
||||
</details>
|
||||
<details><summary>Accès machine <span id="tools-badge" style="margin-left:auto;font-size:10px"></span></summary>
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:12px;padding:4px 0"><input type="checkbox" id="tools-toggle" onchange="toggleTools()"> activer l'accès machine</label>
|
||||
<div class="muted" style="font-size:11px">⚠️ donne à l'IA un accès shell complet au serveur — elle peut inspecter, lancer des scripts et tout faire via le terminal (timeout 30s, max 300s)</div>
|
||||
</details>
|
||||
<details><summary>Skills <span id="skills-badge" style="margin-left:auto;font-size:10px"></span><button class="preset-add" onclick="event.preventDefault();event.stopPropagation();openSkill('')" title="ajouter">+</button></summary>
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:12px;padding:4px 0"><input type="checkbox" id="skills-toggle" onchange="toggleSkills()"> activer</label>
|
||||
<details><summary>Mode agent <span id="agent-badge" style="margin-left:auto;font-size:10px"></span></summary>
|
||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="agent-toggle" onchange="toggleAgent()"><span class="slider"></span></span> activer le mode agent</label>
|
||||
<div class="muted" style="font-size:11px">⚠️ active l'<b>accès machine</b> (shell complet sur le serveur) <b>et</b> les <b>skills</b> — l'IA peut inspecter, lancer des scripts et exécuter ses skills (timeout 30s, max 300s)</div>
|
||||
<div class="skillshead"><span>Skills</span><button class="addbtn" onclick="event.preventDefault();event.stopPropagation();openSkill('')" title="ajouter un skill">+ ajouter</button></div>
|
||||
<div id="skills-list" class="muted" style="font-size:12px"></div>
|
||||
</details>
|
||||
<details><summary>System prompt</summary>
|
||||
@@ -188,8 +213,11 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
<div id="composer">
|
||||
<div id="ctxtrack"><div id="ctx-fill"></div></div>
|
||||
<div id="ctxmeta">
|
||||
<span id="ctx-text" class="muted">contexte 0 / 0</span>
|
||||
<button id="ctx-compact" onclick="compactContext()">compacter</button>
|
||||
<span id="status-preset" class="muted" style="font-size:10px;letter-spacing:.03em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></span>
|
||||
<span style="display:flex;align-items:center;gap:10px;margin-left:auto">
|
||||
<span id="ctx-text" class="muted">contexte 0 / 0</span>
|
||||
<button id="ctx-compact" onclick="compactContext()">compacter</button>
|
||||
</span>
|
||||
</div>
|
||||
<div id="inputbar">
|
||||
<textarea id="input" placeholder="message…" onkeydown="onKey(event)"></textarea>
|
||||
@@ -298,8 +326,9 @@ async function jget(u){ const r=await jfetch(u); return r.json(); }
|
||||
async function jpost(u,b){ const r=await jfetch(u,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})}); return r.json(); }
|
||||
async function loadStatus(){
|
||||
const s=await jget('/api/status');
|
||||
const t=s.active?'<span class="tag ok">● active</span>':'<span class="tag err">○ '+s.state+'</span>';
|
||||
document.getElementById('status-svc').innerHTML = t + ' <span class="muted">:'+s.port+'</span>';
|
||||
const el=document.getElementById('status-svc');
|
||||
el.className='statuspill '+(s.active?'ok':'err');
|
||||
el.innerHTML='<span class="dot"></span>'+(s.active?'active':s.state)+' <span class="port">:'+s.port+'</span>';
|
||||
if(s.ctx){ CTX_MAX=s.ctx; updateCtxMeter(); }
|
||||
}
|
||||
// Compteur de contexte : CTX_USED estimé via les stats serveur (prefill+decode
|
||||
@@ -338,12 +367,8 @@ async function loadPresets(){
|
||||
// Build via DOM (not string concat) so preset names can contain anything —
|
||||
// spaces, accents, quotes, < > & — without breaking markup or handlers.
|
||||
const sp = document.getElementById('status-preset');
|
||||
sp.innerHTML='';
|
||||
if(act){
|
||||
const tag=document.createElement('span');
|
||||
tag.className='tag'; tag.style.borderColor='var(--accent)'; tag.style.color='var(--accent)';
|
||||
tag.textContent=act.name; sp.appendChild(tag);
|
||||
}
|
||||
sp.textContent = act ? act.name : '';
|
||||
sp.title = act ? 'preset actif' : '';
|
||||
const cont=document.getElementById('presets');
|
||||
cont.innerHTML='';
|
||||
if(!p.length){ cont.innerHTML='<span class="muted">(aucun)</span>'; return; }
|
||||
@@ -383,31 +408,28 @@ async function loadPresets(){
|
||||
cont.appendChild(row);
|
||||
});
|
||||
}
|
||||
async function loadSkills(){
|
||||
const s=await jget('/api/skills');
|
||||
document.getElementById('skills-toggle').checked = s.enabled;
|
||||
document.getElementById('skills-badge').innerHTML = s.enabled
|
||||
// « Mode agent » = accès machine + skills réunis en un seul interrupteur.
|
||||
// Quand il est actif, un « a » blanc apparaît en fondu devant « jean » → « ajean ».
|
||||
async function loadAgent(){
|
||||
const s=await jget('/api/agent');
|
||||
const on = s.enabled;
|
||||
document.getElementById('agent-toggle').checked = on;
|
||||
document.getElementById('agent-badge').innerHTML = on
|
||||
? '<span class="tag ok">on</span>' : '<span class="tag" style="opacity:.6">off</span>';
|
||||
document.getElementById('brand').classList.toggle('agent', on);
|
||||
document.getElementById('skills-list').innerHTML = (s.skills||[]).length
|
||||
? s.skills.map(x=>'<div class="preset" style="font-size:12px" onclick="openSkill(\''+x.name+'\')"><span><b style="color:var(--text)">'+x.name+'</b> <span class="muted">— '+x.desc+'</span></span><button onclick="event.stopPropagation();openSkill(\''+x.name+'\')" style="margin:0;padding:2px 8px;font-size:11px">edit</button></div>').join('')
|
||||
: '<div class="muted">(aucun skill)</div>';
|
||||
}
|
||||
async function toggleSkills(){
|
||||
const on=document.getElementById('skills-toggle').checked;
|
||||
await jpost('/api/skills/toggle',{on}); loadSkills();
|
||||
// alias : plusieurs appelants rafraîchissent juste la liste des skills
|
||||
const loadSkills = loadAgent;
|
||||
async function toggleAgent(){
|
||||
const on=document.getElementById('agent-toggle').checked;
|
||||
if(on && !confirm("Activer le mode agent ? L'IA aura un accès shell complet au serveur et pourra exécuter ses skills.")){ document.getElementById('agent-toggle').checked=false; return; }
|
||||
await jpost('/api/agent/toggle',{on});
|
||||
loadAgent();
|
||||
}
|
||||
async function loadTools(){
|
||||
const t=await jget('/api/tools');
|
||||
document.getElementById('tools-toggle').checked = t.enabled;
|
||||
document.getElementById('tools-badge').innerHTML = t.enabled
|
||||
? '<span class="tag ok">on</span>' : '<span class="tag" style="opacity:.6">off</span>';
|
||||
}
|
||||
async function toggleTools(){
|
||||
const on=document.getElementById('tools-toggle').checked;
|
||||
if(on && !confirm("Activer l'accès machine ? L'IA aura un accès shell complet au serveur.")){ document.getElementById('tools-toggle').checked=false; return; }
|
||||
await jpost('/api/tools/toggle',{on}); loadTools();
|
||||
}
|
||||
async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadCfg(),loadPresets(),loadSkills(),loadTools()]); }
|
||||
async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadCfg(),loadPresets(),loadAgent()]); }
|
||||
async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); }
|
||||
function openBenchModal(){ document.getElementById('bench-modal').style.display = 'flex'; }
|
||||
function closeBenchModal(){ document.getElementById('bench-modal').style.display = 'none'; }
|
||||
|
||||
+3
-3
@@ -3,13 +3,13 @@
|
||||
"FileVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 2,
|
||||
"Patch": 9,
|
||||
"Patch": 10,
|
||||
"Build": 0
|
||||
},
|
||||
"ProductVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 2,
|
||||
"Patch": 9,
|
||||
"Patch": 10,
|
||||
"Build": 0
|
||||
},
|
||||
"FileFlagsMask": "3f",
|
||||
@@ -25,7 +25,7 @@
|
||||
"LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.",
|
||||
"OriginalFilename": "jean.exe",
|
||||
"ProductName": "Jean",
|
||||
"ProductVersion": "0.2.9",
|
||||
"ProductVersion": "0.2.10",
|
||||
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
|
||||
},
|
||||
"VarFileInfo": {
|
||||
|
||||
@@ -83,13 +83,18 @@ func newWebMux() *http.ServeMux {
|
||||
api("/api/preset", handlePreset)
|
||||
api("/api/preset/save", handlePresetSave)
|
||||
api("/api/preset/delete", handlePresetDelete)
|
||||
api("/api/skills", handleSkills)
|
||||
api("/api/skills/toggle", handleSkillsToggle)
|
||||
api("/api/agent", handleAgent)
|
||||
api("/api/agent/toggle", handleAgentToggle)
|
||||
// Alias rétro-compat : l'ancien portail ajean.link (dépôt jean-relay) pilote
|
||||
// encore l'agent via /api/tools* et /api/skills/toggle à travers le tunnel E2E.
|
||||
// On les mappe sur le mode agent unifié le temps que le portail soit mis à jour.
|
||||
api("/api/tools", handleAgent)
|
||||
api("/api/tools/toggle", handleAgentToggle)
|
||||
api("/api/skills", handleAgent)
|
||||
api("/api/skills/toggle", handleAgentToggle)
|
||||
api("/api/skill", handleSkill)
|
||||
api("/api/skill/save", handleSkillSave)
|
||||
api("/api/skill/delete", handleSkillDelete)
|
||||
api("/api/tools", handleTools)
|
||||
api("/api/tools/toggle", handleToolsToggle)
|
||||
api("/api/switch", handleSwitch)
|
||||
api("/api/start", svcHandler("start"))
|
||||
api("/api/stop", svcHandler("stop"))
|
||||
@@ -419,25 +424,27 @@ func handlePresetDelete(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, 200, map[string]any{"ok": true, "modelDeleted": modelDeleted, "modelError": modelErr})
|
||||
}
|
||||
|
||||
func handleSkills(w http.ResponseWriter, r *http.Request) {
|
||||
// handleAgent renvoie l'état du mode agent ET la liste des skills (qui sont des
|
||||
// outils gérés sous ce même interrupteur) — un seul aller-retour pour l'UI.
|
||||
func handleAgent(w http.ResponseWriter, r *http.Request) {
|
||||
sk := ListSkills()
|
||||
out := []map[string]any{}
|
||||
for _, s := range sk {
|
||||
out = append(out, map[string]any{"name": s.Name, "desc": s.Desc})
|
||||
}
|
||||
sendJSON(w, 200, map[string]any{"enabled": skillsEnabled(), "skills": out})
|
||||
sendJSON(w, 200, map[string]any{"enabled": agentEnabled(), "skills": out})
|
||||
}
|
||||
|
||||
func handleSkillsToggle(w http.ResponseWriter, r *http.Request) {
|
||||
func handleAgentToggle(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
On bool `json:"on"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
if err := setSkillsEnabled(req.On); err != nil {
|
||||
if err := setAgentEnabled(req.On); err != nil {
|
||||
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
sendJSON(w, 200, map[string]any{"ok": true, "enabled": skillsEnabled()})
|
||||
sendJSON(w, 200, map[string]any{"ok": true, "enabled": agentEnabled()})
|
||||
}
|
||||
|
||||
func handleSkill(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -480,22 +487,6 @@ func handleSkillDelete(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, 200, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
func handleTools(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, 200, map[string]any{"enabled": toolsEnabled()})
|
||||
}
|
||||
|
||||
func handleToolsToggle(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
On bool `json:"on"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
if err := setToolsEnabled(req.On); err != nil {
|
||||
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
sendJSON(w, 200, map[string]any{"ok": true, "enabled": toolsEnabled()})
|
||||
}
|
||||
|
||||
func handleSwitch(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
N int `json:"n"`
|
||||
@@ -576,9 +567,11 @@ func handleBenchLast(w http.ResponseWriter, r *http.Request) {
|
||||
type chatReq struct {
|
||||
Messages []Message `json:"messages"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
// Optional per-request capability overrides (used by ajean.link agents,
|
||||
// which carry their own tools/skills toggles). nil = inherit the
|
||||
// machine's global config.
|
||||
// Optional per-request override of the agent mode (used by ajean.link
|
||||
// agents, which carry their own toggle). nil = inherit the machine's
|
||||
// global config. Tools/Skills sont conservés pour la rétro-compat des
|
||||
// anciens clients relais : l'un OU l'autre à true active le mode agent.
|
||||
Agent *bool `json:"agent"`
|
||||
Tools *bool `json:"tools"`
|
||||
Skills *bool `json:"skills"`
|
||||
}
|
||||
@@ -623,11 +616,11 @@ func runChatStream(ctx context.Context, body chatReq, emit func(map[string]any)
|
||||
body.Temperature = 0.7
|
||||
}
|
||||
caps := globalCaps()
|
||||
if body.Tools != nil {
|
||||
caps.Tools = *body.Tools
|
||||
}
|
||||
if body.Skills != nil {
|
||||
caps.Skills = *body.Skills
|
||||
if body.Agent != nil {
|
||||
caps.Agent = *body.Agent
|
||||
} else if body.Tools != nil || body.Skills != nil {
|
||||
// rétro-compat : anciens clients qui envoyaient deux drapeaux séparés
|
||||
caps.Agent = (body.Tools != nil && *body.Tools) || (body.Skills != nil && *body.Skills)
|
||||
}
|
||||
msgs := InjectSkills(body.Messages, caps)
|
||||
extra, _ := runChat(ctx, msgs, body.Temperature, caps, func(ev StreamEvent) bool {
|
||||
|
||||
Reference in new issue
Block a user