Compare commits

...
Author SHA1 Message Date
Claude 5e79a98206 chore: version 2.5.4 (la 2.5.3 est prise par master)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMpgFriwxiBgurUVb21oCE
2026-09-05 09:24:19 +00:00
Claude 27985c0740 Merge remote-tracking branch 'origin/master' into claude/french-voice-system-16jr85 2026-09-05 09:23:30 +00:00
Claude e6437f4bb2 feat: effort de raisonnement Hermes complet sur les trois transports (v2.5.3)
- Huit niveaux (none, minimal, low, medium, high, xhigh, max, ultra) au lieu de
  low / medium / high, avec libellés explicatifs dans Paramètres → Hermes.
- model_options.reasoning_effort envoyé sur runs, sessions et chat completions ;
  il ne l'était qu'en chat completions.
- Test des trois transports (effort présent, absent quand laissé au serveur).
- Le sélecteur de modèle par fournisseur ayant été livré sur master en 2.5.2,
  cette branche ne garde que l'effort de raisonnement.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMpgFriwxiBgurUVb21oCE
2026-09-05 08:20:48 +00:00
8 changed files with 81 additions and 12 deletions

No files matched your search

+2 -1
View File
@@ -1,7 +1,7 @@
# EveFlow 2 — Interface vocale JARVIS pour Hermes Agent
[![Build](https://img.shields.io/github/actions/workflow/status/R0m1k3/EveFlow/windows-release.yml?style=flat-square)](https://github.com/R0m1k3/EveFlow/actions)
[![Version](https://img.shields.io/badge/version-2.5.0-brightgreen.svg?style=flat-square)](https://github.com/R0m1k3/EveFlow/releases)
[![Version](https://img.shields.io/badge/version-2.5.4-brightgreen.svg?style=flat-square)](https://github.com/R0m1k3/EveFlow/releases)
[![License](https://img.shields.io/badge/license-MIT-lightgrey.svg?style=flat-square)](LICENSE)
**EveFlow** est un compagnon de bureau Windows qui transforme [Hermes Agent](https://hermes-agent.nousresearch.com/) en assistant vocal à la JARVIS : un noyau holographique réactif au son, une conversation en streaming, les outils, sous-agents, approbations, crons, skills et sessions d'Hermes pilotés depuis un seul HUD.
@@ -46,6 +46,7 @@ La version 2 est une réécriture complète : plus de robot 3D, un pipeline voca
1. **Runs API** (`POST /v1/runs` + `GET /v1/runs/{id}/events`) : deltas, outils, sous-agents, `approval.request`, `run.completed`, arrêt (`/stop`) et injection de consignes en cours de run (`/steer`).
2. **Sessions API** (`/api/sessions/{id}/chat/stream`) : mémoire côté serveur, fork, suppression, relecture de l'historique.
3. **Chat completions** OpenAI (`/v1/chat/completions`) avec `hermes.tool.progress`, continuité de session (`X-Hermes-Session-Id`) et outils EveFlow côté client (état du HUD, fichiers partagés, notifications).
* **Effort de raisonnement** : les huit niveaux d'Hermes (none, minimal, low, medium, high, xhigh, max, ultra) au choix dans Paramètres → Hermes, envoyés dans `model_options.reasoning_effort` sur les trois transports (runs, sessions, chat completions).
* Mémoire longue durée via `X-Hermes-Session-Key`.
* **Approbations** d'outils affichées dans le HUD : une fois, pour la session, toujours, refuser.
* **Crons** : création en langage naturel (`every 1h`, `weekdays at 9am`, `in 30m`, expression cron), pause/reprise, exécution immédiate, édition, historique des résultats lus à voix haute.
+1
View File
@@ -84,6 +84,7 @@ Sources : [jarvis-desktop-ai](https://github.com/ccarloshenri/jarvis-desktop-ai)
| Correctifs 2.4.0.1 | réponse vide en chat completions désormais expliquée (JSON non streamé ou erreur HTTP 200), la liaison ne passe plus en « dégradé » quand seule l'API des crons échoue, transcriptions parasites (« (cliquant) », « *Claire* ») ignorées, préférence de voix masculine/féminine |
| Parakeet v3 vs Whisper base sur trois phrases Piper (fr) (2.4.1) | Parakeet : 3/3 exactes avec ponctuation, 0,4 à 0,5 s à chaud (6,7 s au premier appel) ; Whisper base : erreurs sur « Jarvis », « Peux-tu », 0,7 à 0,9 s |
| Worklets audio sous CSP stricte (2.4.1) | chargement des modules statiques OK dans l'application empaquetée |
| Effort de raisonnement Hermes (2.5.4) | huit niveaux (none → ultra) dans les réglages, transmis dans `model_options.reasoning_effort` sur runs, sessions et chat completions (auparavant chat completions seulement, trois niveaux) ; test des trois transports |
| Voix françaises (2.5.0) | Edge Henri : MP3 reçu de bout en bout (32 ko pour 4 s). Supertonic 3 en français : 10 voix, RTF 0,14 sur 4 cœurs (8,7 s d'audio en 1,3 s), genres déterminés par mesure de la fréquence fondamentale (voix 0-4 : 170-210 Hz, voix 5-9 : 92-137 Hz) ; le worker compilé accepte `language` et bascule sur l'anglais pour une langue inconnue |
| Reconnaissance française : Parakeet v3 vs Qwen3-ASR 0.6B int8 (2.5.0) | Six phrases Supertonic (3,8 s) : Parakeet WER 8,5 % (erreurs surtout de forme : « 14h30 »), 384 ms par phrase ; Qwen3-ASR WER 15,3 % (« mémoires vivres »), 1 477 ms, 940 Mo. Qwen3-ASR n'est pas ajouté au catalogue |
| Serveur MCP (2.4.0) | initialize, tools/list (14 outils), tools/call côté principal (presse-papiers, capture image) et côté renderer (état, message dans le fil) |
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "eveflow",
"version": "2.5.3",
"releaseVersion": "2.5.3",
"version": "2.5.4",
"releaseVersion": "2.5.4",
"description": "JARVIS-style desktop HUD for Hermes Agent: voice, streaming runs, scheduled jobs, skills and telemetry",
"main": "dist-electron/main.js",
"private": true,
+4 -5
View File
@@ -16,6 +16,7 @@ import { useVoice } from '../../state/voice';
import { installedModels, useVoiceModels } from '../../state/voiceModels';
import { ModelsSection } from './ModelsSection';
import { ModelSelect } from './ModelSelect';
import { REASONING_EFFORTS, isReasoningEffort } from '../../lib/hermesReasoning';
type Section = 'general' | 'hermes' | 'voice' | 'speech' | 'models' | 'webhook' | 'notifications' | 'ui';
@@ -244,12 +245,10 @@ export function SettingsDrawer({ onClose }: Props) {
</div>
<div className="field">
<label>Effort de raisonnement</label>
<select className="select" value={settings.hermes.reasoningEffort} onChange={(e) => update({ hermes: { reasoningEffort: e.target.value as typeof settings.hermes.reasoningEffort } })}>
<option value="">Défaut</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<select className="select" value={settings.hermes.reasoningEffort} onChange={(e) => { if (isReasoningEffort(e.target.value)) update({ hermes: { reasoningEffort: e.target.value } }); }}>
{REASONING_EFFORTS.map((e) => <option key={e.value} value={e.value}>{e.label}</option>)}
</select>
<span className="hint">Envoyé à chaque requête (runs, sessions, chat completions) dans model_options.reasoning_effort. Plus c’est haut, plus la réponse est réfléchie et lente.</span>
</div>
</div>
<div className="field">
+18
View File
@@ -0,0 +1,18 @@
/** Reasoning effort levels accepted by Hermes (`agent.reasoning_effort` / `model_options.reasoning_effort`). */
import type { HermesReasoningEffort } from '../services/hermes/types';
export const REASONING_EFFORTS: Array<{ value: HermesReasoningEffort; label: string }> = [
{ value: '', label: 'Défaut du serveur (medium)' },
{ value: 'none', label: 'none : pas de réflexion' },
{ value: 'minimal', label: 'minimal' },
{ value: 'low', label: 'low : rapide' },
{ value: 'medium', label: 'medium : équilibré' },
{ value: 'high', label: 'high : réponses plus fouillées' },
{ value: 'xhigh', label: 'xhigh' },
{ value: 'max', label: 'max' },
{ value: 'ultra', label: 'ultra : le plus lent' }
];
export function isReasoningEffort(value: string): value is HermesReasoningEffort {
return REASONING_EFFORTS.some((e) => e.value === value);
}
+8 -3
View File
@@ -160,6 +160,11 @@ export class HermesClient {
return hermesBaseUrl(this.config.url);
}
/** `model_options.reasoning_effort`, accepted by runs, session chat and chat completions alike. */
private reasoningOptions(): Rec {
return this.config.reasoningEffort ? { model_options: { reasoning_effort: this.config.reasoningEffort } } : {};
}
private headers(extra: Record<string, string> = {}, json = true): Record<string, string> {
const h: Record<string, string> = { ...extra };
if (json) h['Content-Type'] = 'application/json';
@@ -335,7 +340,7 @@ export class HermesClient {
// ── Runs ──────────────────────────────────────────────────────────────────
async startRun(body: { input: string; session_id?: string; instructions?: string; model?: string; provider?: string }): Promise<{ run_id: string; status: string }> {
const payload: Rec = { input: body.input };
const payload: Rec = { input: body.input, ...this.reasoningOptions() };
if (body.session_id) payload.session_id = body.session_id;
if (body.instructions) payload.instructions = body.instructions;
if (body.model) payload.model = body.model;
@@ -495,7 +500,7 @@ export class HermesClient {
}
if (isAborted()) return '';
const realId = sessionId.slice(3);
const body: Rec = { input: options.text };
const body: Rec = { input: options.text, ...this.reasoningOptions() };
if (this.config.instructions) body.instructions = this.config.instructions;
Object.assign(body, modelSelection(this.config.model));
@@ -539,7 +544,7 @@ export class HermesClient {
payload.tools = options.localToolDefinitions;
payload.tool_choice = 'auto';
}
if (this.config.reasoningEffort) payload.model_options = { reasoning_effort: this.config.reasoningEffort };
Object.assign(payload, this.reasoningOptions());
const toolCalls = new Map<number, CompletionToolCallAccumulator>();
let finishReason: string | null = null;
+4 -1
View File
@@ -1,5 +1,8 @@
export type HermesTransport = 'auto' | 'runs' | 'sessions' | 'completions';
/** Thinking depth accepted by Hermes (`model_options.reasoning_effort`); empty = server default (medium). */
export type HermesReasoningEffort = '' | 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'ultra';
export interface HermesConfig {
url: string;
apiKey: string;
@@ -7,7 +10,7 @@ export interface HermesConfig {
/** Stable per-user key for long-term memory (X-Hermes-Session-Key). */
sessionKey: string;
transport: HermesTransport;
reasoningEffort: '' | 'low' | 'medium' | 'high';
reasoningEffort: HermesReasoningEffort;
/** Extra instructions layered on top of the Hermes system prompt. */
instructions: string;
/** Model used in "mission" mode (long tasks); empty = same as `model`. */
+42
View File
@@ -0,0 +1,42 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { HermesClient } from '../src/services/hermes/client';
import { httpFetch, httpStream } from '../src/lib/transport';
import { DEFAULT_SETTINGS } from '../src/state/settings';
import { REASONING_EFFORTS, isReasoningEffort } from '../src/lib/hermesReasoning';
vi.mock('../src/lib/transport', async (original) => ({ ...await original<typeof import('../src/lib/transport')>(), httpFetch: vi.fn(), httpStream: vi.fn() }));
const config = { ...DEFAULT_SETTINGS.hermes, url: 'https://example.test', apiKey: 'k' };
afterEach(() => vi.restoreAllMocks());
describe('reasoning effort levels', () => {
it('lists every level Hermes accepts, server default first', () => {
expect(REASONING_EFFORTS.map((e) => e.value)).toEqual(['', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
expect(isReasoningEffort('xhigh')).toBe(true);
expect(isReasoningEffort('')).toBe(true);
expect(isReasoningEffort('turbo')).toBe(false);
});
it.each(['runs', 'sessions', 'completions'] as const)('sends model_options.reasoning_effort over %s', async (transport) => {
const requests: Record<string, unknown>[] = [];
const capture = async (req: { body?: string }) => {
requests.push(JSON.parse(req.body ?? '{}') as Record<string, unknown>);
throw new Error('stop');
};
vi.mocked(httpFetch).mockImplementation(capture as never);
vi.mocked(httpStream).mockImplementation(capture as never);
const send = new HermesClient({ ...config, reasoningEffort: 'high' }).send({ text: 'Bonjour', sessionId: 'hs:test', history: [], onEvent: vi.fn() }, transport);
await expect(send.result).rejects.toThrow('stop');
expect(requests[0]).toMatchObject({ model_options: { reasoning_effort: 'high' } });
});
it('omits model_options when the effort is left to the server', async () => {
const requests: Record<string, unknown>[] = [];
vi.mocked(httpFetch).mockImplementation((async (req: { body?: string }) => {
requests.push(JSON.parse(req.body ?? '{}') as Record<string, unknown>);
throw new Error('stop');
}) as never);
const send = new HermesClient({ ...config, reasoningEffort: '' }).send({ text: 'Bonjour', sessionId: 'test', history: [], onEvent: vi.fn() }, 'runs');
await expect(send.result).rejects.toThrow('stop');
expect(requests[0]).not.toHaveProperty('model_options');
});
});