From 2a23b682be4d0b44985a34debef0c34b17e7197e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Tue, 26 May 2026 04:42:28 +0800 Subject: [PATCH] feat(hermes): add agent quality config --- scripts/dev-api.js | 52 +++++++ src-tauri/src/commands/hermes.rs | 175 +++++++++++++++++++++- src/engines/hermes/pages/config.js | 21 +++ src/locales/modules/engine.js | 11 +- tests/hermes-agent-runtime-config.test.js | 55 +++++++ tests/hermes-config-page-ui.test.js | 3 + 6 files changed, 315 insertions(+), 2 deletions(-) diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 2901441..a652d33 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -3334,6 +3334,7 @@ const HERMES_APPROVAL_MODES = new Set(['manual', 'smart', 'off']) const HERMES_APPROVAL_CRON_MODES = new Set(['deny', 'approve']) const HERMES_LOGGING_LEVELS = new Set(['DEBUG', 'INFO', 'WARNING']) const HERMES_AGENT_IMAGE_INPUT_MODES = new Set(['auto', 'native', 'text']) +const HERMES_AGENT_REASONING_EFFORTS = new Set(['xhigh', 'high', 'medium', 'low', 'minimal', 'none']) const HERMES_PROMPT_CACHE_TTLS = new Set(['5m', '1h']) const HERMES_PROVIDER_ROUTING_SORTS = new Set(['price', 'throughput', 'latency']) const HERMES_PROVIDER_ROUTING_DATA_COLLECTION = new Set(['allow', 'deny']) @@ -3514,6 +3515,13 @@ function normalizeHermesImageInputMode(value, strict = false) { return 'auto' } +function normalizeHermesReasoningEffort(value, strict = false) { + const effort = String(value ?? '').trim().toLowerCase() || 'medium' + if (HERMES_AGENT_REASONING_EFFORTS.has(effort)) return effort + if (strict) throw new Error('agent.reasoning_effort 必须是 xhigh、high、medium、low、minimal 或 none') + return 'medium' +} + function normalizeHermesPromptCacheTtl(value, strict = false) { const ttl = String(value ?? '').trim().toLowerCase() || '5m' if (HERMES_PROMPT_CACHE_TTLS.has(ttl)) return ttl @@ -4018,6 +4026,39 @@ function normalizeHermesToolsetList(value, fieldName = 'agent.disabled_toolsets' return normalized } +function validateHermesPersonalities(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new Error('agent.personalities 必须是 JSON 对象') + } + const normalized = {} + for (const [rawName, rawPrompt] of Object.entries(value)) { + const name = String(rawName || '').trim() + if (!name) throw new Error('agent.personalities 名称不能为空') + if (!/^[A-Za-z0-9_.-]+$/.test(name)) { + throw new Error(`agent.personalities.${name} 名称只能包含字母、数字、下划线、点和短横线`) + } + if (typeof rawPrompt !== 'string') { + throw new Error(`agent.personalities.${name} 必须是字符串`) + } + const prompt = rawPrompt.trim() + if (!prompt) throw new Error(`agent.personalities.${name} 不能为空`) + normalized[name] = prompt + } + return normalized +} + +function parseHermesPersonalitiesJson(raw) { + const text = String(raw ?? '').trim() + if (!text) return {} + let value + try { + value = JSON.parse(text) + } catch (err) { + throw new Error(`agent.personalities JSON 格式错误: ${err.message}`) + } + return validateHermesPersonalities(value) +} + function validateHermesPlatformToolsets(value) { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new Error('platform_toolsets 必须是 JSON 对象') @@ -4127,6 +4168,9 @@ export function buildHermesAgentRuntimeConfigValues(config = {}) { const agent = root.agent && typeof root.agent === 'object' && !Array.isArray(root.agent) ? root.agent : {} + const personalities = agent.personalities && typeof agent.personalities === 'object' && !Array.isArray(agent.personalities) + ? validateHermesPersonalities(agent.personalities) + : {} return { agentMaxTurns: parseHermesInteger(agent.max_turns, 'agent.max_turns', 90, 1, 10000, false), gatewayTimeout: parseHermesInteger(agent.gateway_timeout, 'agent.gateway_timeout', 1800, 0, 604800, false), @@ -4137,6 +4181,9 @@ export function buildHermesAgentRuntimeConfigValues(config = {}) { gatewayNotifyInterval: parseHermesInteger(agent.gateway_notify_interval, 'agent.gateway_notify_interval', 180, 0, 86400, false), gatewayAutoContinueFreshness: parseHermesInteger(agent.gateway_auto_continue_freshness, 'agent.gateway_auto_continue_freshness', 3600, 0, 604800, false), imageInputMode: normalizeHermesImageInputMode(agent.image_input_mode, false), + agentVerbose: readHermesBool(agent.verbose, false), + reasoningEffort: normalizeHermesReasoningEffort(agent.reasoning_effort, false), + personalitiesJson: JSON.stringify(personalities, null, 2), } } @@ -4155,6 +4202,11 @@ export function mergeHermesAgentRuntimeConfig(config = {}, form = {}) { agent.gateway_notify_interval = parseHermesInteger(Object.hasOwn(form, 'gatewayNotifyInterval') ? form.gatewayNotifyInterval : currentValues.gatewayNotifyInterval, 'agent.gateway_notify_interval', 180, 0, 86400, true) agent.gateway_auto_continue_freshness = parseHermesInteger(Object.hasOwn(form, 'gatewayAutoContinueFreshness') ? form.gatewayAutoContinueFreshness : currentValues.gatewayAutoContinueFreshness, 'agent.gateway_auto_continue_freshness', 3600, 0, 604800, true) agent.image_input_mode = normalizeHermesImageInputMode(Object.hasOwn(form, 'imageInputMode') ? form.imageInputMode : currentValues.imageInputMode, true) + agent.verbose = formHermesBool(form, 'agentVerbose', currentValues.agentVerbose) + agent.reasoning_effort = normalizeHermesReasoningEffort(Object.hasOwn(form, 'reasoningEffort') ? form.reasoningEffort : currentValues.reasoningEffort, true) + const personalities = parseHermesPersonalitiesJson(Object.hasOwn(form, 'personalitiesJson') ? form.personalitiesJson : currentValues.personalitiesJson) + if (Object.keys(personalities).length) agent.personalities = personalities + else delete agent.personalities next.agent = agent return next } diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs index 75072a7..5051e74 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -5242,6 +5242,71 @@ fn normalize_hermes_image_input_mode( } } +fn normalize_hermes_reasoning_effort( + value: Option, + strict: bool, +) -> Result { + let effort = value.unwrap_or_default().trim().to_ascii_lowercase(); + let effort = if effort.is_empty() { + "medium".to_string() + } else { + effort + }; + if matches!( + effort.as_str(), + "xhigh" | "high" | "medium" | "low" | "minimal" | "none" + ) { + return Ok(effort); + } + if strict { + Err("agent.reasoning_effort 必须是 xhigh、high、medium、low、minimal 或 none".to_string()) + } else { + Ok("medium".to_string()) + } +} + +fn validate_hermes_personalities(value: &Value) -> Result, String> { + let Some(map) = value.as_object() else { + return Err("agent.personalities 必须是 JSON 对象".to_string()); + }; + let mut normalized = serde_json::Map::new(); + for (raw_name, raw_prompt) in map { + let name = raw_name.trim(); + if name.is_empty() { + return Err("agent.personalities 名称不能为空".to_string()); + } + if !name + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '.' | '-')) + { + return Err(format!( + "agent.personalities.{name} 名称只能包含字母、数字、下划线、点和短横线" + )); + } + let Some(prompt) = raw_prompt.as_str() else { + return Err(format!("agent.personalities.{name} 必须是字符串")); + }; + let prompt = prompt.trim(); + if prompt.is_empty() { + return Err(format!("agent.personalities.{name} 不能为空")); + } + normalized.insert(name.to_string(), Value::String(prompt.to_string())); + } + Ok(normalized) +} + +fn parse_hermes_personalities_json( + raw: Option, +) -> Result, String> { + let text = raw.unwrap_or_default().trim().to_string(); + if text.is_empty() { + return Ok(serde_json::Map::new()); + } + let value: Value = serde_json::from_str(&text) + .map_err(|err| format!("agent.personalities JSON 格式错误: {err}"))?; + validate_hermes_personalities(&value) +} + fn build_hermes_agent_runtime_config_values(config: &serde_yaml::Value) -> Value { let root = config.as_mapping(); let agent = root.and_then(|map| yaml_get_mapping(map, "agent")); @@ -5251,6 +5316,16 @@ fn build_hermes_agent_runtime_config_values(config: &serde_yaml::Value) -> Value false, ) .unwrap_or_else(|_| "auto".to_string()); + let reasoning_effort = normalize_hermes_reasoning_effort( + agent.and_then(|map| yaml_string_field(map, "reasoning_effort")), + false, + ) + .unwrap_or_else(|_| "medium".to_string()); + let personalities = agent + .and_then(|map| yaml_get(map, "personalities")) + .and_then(|value| serde_json::to_value(value).ok()) + .and_then(|value| validate_hermes_personalities(&value).ok()) + .unwrap_or_default(); serde_json::json!({ "agentMaxTurns": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "max_turns"), 90, 1, 10000)).unwrap_or(90), @@ -5262,6 +5337,9 @@ fn build_hermes_agent_runtime_config_values(config: &serde_yaml::Value) -> Value "gatewayNotifyInterval": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "gateway_notify_interval"), 180, 0, 86400)).unwrap_or(180), "gatewayAutoContinueFreshness": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "gateway_auto_continue_freshness"), 3600, 0, 604800)).unwrap_or(3600), "imageInputMode": image_input_mode, + "agentVerbose": agent.and_then(|map| yaml_bool_field(map, "verbose")).unwrap_or(false), + "reasoningEffort": reasoning_effort, + "personalitiesJson": serde_json::to_string_pretty(&Value::Object(personalities)).unwrap_or_else(|_| "{}".to_string()), }) } @@ -5347,6 +5425,22 @@ fn merge_hermes_agent_runtime_config( }, true, )?; + let agent_verbose = form_bool(form, "agentVerbose") + .unwrap_or_else(|| current["agentVerbose"].as_bool().unwrap_or(false)); + let reasoning_effort = normalize_hermes_reasoning_effort( + if form.get("reasoningEffort").is_some() { + form_string(form, "reasoningEffort") + } else { + current["reasoningEffort"].as_str().map(ToString::to_string) + }, + true, + )?; + let personalities = + parse_hermes_personalities_json(form_string(form, "personalitiesJson").or_else(|| { + current["personalitiesJson"] + .as_str() + .map(ToString::to_string) + }))?; let root = ensure_yaml_object(config)?; let agent = yaml_child_object(root, "agent")?; @@ -5386,6 +5480,18 @@ fn merge_hermes_agent_runtime_config( yaml_key("image_input_mode"), serde_yaml::Value::String(image_input_mode), ); + agent.insert(yaml_key("verbose"), serde_yaml::Value::Bool(agent_verbose)); + agent.insert( + yaml_key("reasoning_effort"), + serde_yaml::Value::String(reasoning_effort), + ); + if personalities.is_empty() { + agent.remove(yaml_key("personalities")); + } else { + let yaml_value = serde_yaml::to_value(Value::Object(personalities)) + .map_err(|err| format!("agent.personalities 转换 YAML 失败: {err}"))?; + agent.insert(yaml_key("personalities"), yaml_value); + } Ok(()) } @@ -17290,7 +17396,7 @@ agent: #[cfg(test)] mod hermes_agent_runtime_config_tests { use super::{build_hermes_agent_runtime_config_values, merge_hermes_agent_runtime_config}; - use serde_json::json; + use serde_json::{json, Value}; #[test] fn agent_runtime_values_have_upstream_defaults() { @@ -17305,6 +17411,9 @@ mod hermes_agent_runtime_config_tests { assert_eq!(values["gatewayNotifyInterval"], 180); assert_eq!(values["gatewayAutoContinueFreshness"], 3600); assert_eq!(values["imageInputMode"], "auto"); + assert_eq!(values["agentVerbose"], false); + assert_eq!(values["reasoningEffort"], "medium"); + assert_eq!(values["personalitiesJson"], "{}"); } #[test] @@ -17321,6 +17430,11 @@ agent: gateway_notify_interval: 240 gateway_auto_continue_freshness: 5400 image_input_mode: native + verbose: true + reasoning_effort: high + personalities: + concise: Keep answers short. + teacher: Explain with examples. "#, ) .unwrap(); @@ -17335,6 +17449,12 @@ agent: assert_eq!(values["gatewayNotifyInterval"], 240); assert_eq!(values["gatewayAutoContinueFreshness"], 5400); assert_eq!(values["imageInputMode"], "native"); + assert_eq!(values["agentVerbose"], true); + assert_eq!(values["reasoningEffort"], "high"); + let personalities: Value = + serde_json::from_str(values["personalitiesJson"].as_str().unwrap()).unwrap(); + assert_eq!(personalities["concise"], "Keep answers short."); + assert_eq!(personalities["teacher"], "Explain with examples."); } #[test] @@ -17366,6 +17486,9 @@ streaming: "gatewayNotifyInterval": "120", "gatewayAutoContinueFreshness": "1800", "imageInputMode": "text", + "agentVerbose": true, + "reasoningEffort": "low", + "personalitiesJson": r#"{"concise":" Keep replies brief. ","ops":"Focus on operational risk."}"#, }), ) .unwrap(); @@ -17390,6 +17513,16 @@ streaming: Some(1800) ); assert_eq!(config["agent"]["image_input_mode"].as_str(), Some("text")); + assert_eq!(config["agent"]["verbose"].as_bool(), Some(true)); + assert_eq!(config["agent"]["reasoning_effort"].as_str(), Some("low")); + assert_eq!( + config["agent"]["personalities"]["concise"].as_str(), + Some("Keep replies brief.") + ); + assert_eq!( + config["agent"]["personalities"]["ops"].as_str(), + Some("Focus on operational risk.") + ); assert_eq!( config["agent"]["disabled_toolsets"][0].as_str(), Some("terminal") @@ -17397,6 +17530,30 @@ streaming: assert_eq!(config["agent"]["custom_flag"].as_str(), Some("keep-agent")); } + #[test] + fn merge_agent_runtime_config_removes_empty_personalities() { + let mut config: serde_yaml::Value = serde_yaml::from_str( + r#" +agent: + personalities: + concise: Keep answers short. + custom_flag: keep-agent +"#, + ) + .unwrap(); + + merge_hermes_agent_runtime_config( + &mut config, + &json!({ + "personalitiesJson": "{}", + }), + ) + .unwrap(); + + assert!(config["agent"].get("personalities").is_none()); + assert_eq!(config["agent"]["custom_flag"].as_str(), Some("keep-agent")); + } + #[test] fn merge_agent_runtime_config_allows_zero_disable_values() { let mut config = serde_yaml::Value::Mapping(serde_yaml::Mapping::new()); @@ -17439,6 +17596,22 @@ streaming: merge_hermes_agent_runtime_config(&mut config, &json!({ "clarifyTimeout": "-1" })) .unwrap_err(); assert!(err.contains("agent.clarify_timeout")); + let err = + merge_hermes_agent_runtime_config(&mut config, &json!({ "reasoningEffort": "max" })) + .unwrap_err(); + assert!(err.contains("agent.reasoning_effort")); + let err = merge_hermes_agent_runtime_config( + &mut config, + &json!({ "personalitiesJson": r#"{"bad name":"x"}"# }), + ) + .unwrap_err(); + assert!(err.contains("agent.personalities.bad name")); + let err = merge_hermes_agent_runtime_config( + &mut config, + &json!({ "personalitiesJson": r#"{"concise":123}"# }), + ) + .unwrap_err(); + assert!(err.contains("agent.personalities.concise")); } } diff --git a/src/engines/hermes/pages/config.js b/src/engines/hermes/pages/config.js index fab0567..22efe57 100644 --- a/src/engines/hermes/pages/config.js +++ b/src/engines/hermes/pages/config.js @@ -118,6 +118,9 @@ const AGENT_RUNTIME_DEFAULTS = { gatewayNotifyInterval: 180, gatewayAutoContinueFreshness: 3600, imageInputMode: 'auto', + agentVerbose: false, + reasoningEffort: 'medium', + personalitiesJson: '{}', } const UNAUTHORIZED_DM_DEFAULTS = { @@ -260,6 +263,7 @@ const STT_OPENAI_MODELS = ['whisper-1', 'gpt-4o-mini-transcribe', 'gpt-4o-transc const STT_MISTRAL_MODELS = ['voxtral-mini-latest', 'voxtral-mini-2602'] const UNAUTHORIZED_DM_BEHAVIORS = ['pair', 'ignore'] const IMAGE_INPUT_MODES = ['auto', 'native', 'text'] +const REASONING_EFFORTS = ['xhigh', 'high', 'medium', 'low', 'minimal', 'none'] const DISPLAY_TOOL_PROGRESS_VALUES = ['off', 'new', 'all', 'verbose'] const DISPLAY_LANGUAGE_VALUES = ['en', 'zh', 'zh-hant', 'ja', 'de', 'es', 'fr', 'tr', 'uk', 'af', 'ko', 'it', 'ga', 'pt', 'ru', 'hu'] const DISPLAY_RESUME_VALUES = ['full', 'minimal'] @@ -1123,6 +1127,20 @@ export function render() { ${IMAGE_INPUT_MODES.map(mode => option(`engine.hermesAgentRuntimeConfigImageInputMode_${mode}`, mode, agentRuntimeValues.imageInputMode)).join('')} + + +
${t('engine.hermesAgentRuntimeConfigFootnote')}
@@ -3060,6 +3078,9 @@ export function render() { gatewayNotifyInterval: el.querySelector('#hm-agent-gateway-notify-interval')?.value || '180', gatewayAutoContinueFreshness: el.querySelector('#hm-agent-gateway-auto-continue-freshness')?.value || '3600', imageInputMode: el.querySelector('#hm-agent-image-input-mode')?.value || 'auto', + reasoningEffort: el.querySelector('#hm-agent-reasoning-effort')?.value || 'medium', + agentVerbose: !!el.querySelector('#hm-agent-verbose')?.checked, + personalitiesJson: el.querySelector('#hm-agent-personalities-json')?.value || '{}', } agentRuntimeSaving = true agentRuntimeError = null diff --git a/src/locales/modules/engine.js b/src/locales/modules/engine.js index 64bbab9..680218b 100644 --- a/src/locales/modules/engine.js +++ b/src/locales/modules/engine.js @@ -897,7 +897,16 @@ export default { hermesAgentRuntimeConfigImageInputMode_auto: _('自动选择', 'Auto', '自動選擇'), hermesAgentRuntimeConfigImageInputMode_native: _('原生图片输入', 'Native image input', '原生圖片輸入'), hermesAgentRuntimeConfigImageInputMode_text: _('转文本描述', 'Convert to text description', '轉文字描述'), - hermesAgentRuntimeConfigFootnote: _('这些字段会写入 agent.*,影响 CLI 与 Gateway 长跑行为。将可选超时设为 0 表示关闭对应限制或通知;disabled_toolsets 和其他高级 agent 字段会保留在 raw YAML 中。', 'These fields are written under agent.* and affect CLI and Gateway long-running behavior. Set optional timeouts to 0 to disable the corresponding limit or notification. disabled_toolsets and other advanced agent fields stay in raw YAML.', '這些欄位會寫入 agent.*,影響 CLI 與 Gateway 長跑行為。將可選逾時設為 0 表示關閉對應限制或通知;disabled_toolsets 和其他進階 agent 欄位會保留在 raw YAML 中。'), + hermesAgentRuntimeConfigVerbose: _('启用详细日志', 'Enable verbose logging', '啟用詳細日誌'), + hermesAgentRuntimeConfigReasoningEffort: _('推理强度', 'Reasoning effort', '推理強度'), + hermesAgentRuntimeConfigReasoningEffort_xhigh: _('最高', 'Extra high', '最高'), + hermesAgentRuntimeConfigReasoningEffort_high: _('高', 'High', '高'), + hermesAgentRuntimeConfigReasoningEffort_medium: _('中', 'Medium', '中'), + hermesAgentRuntimeConfigReasoningEffort_low: _('低', 'Low', '低'), + hermesAgentRuntimeConfigReasoningEffort_minimal: _('极简', 'Minimal', '極簡'), + hermesAgentRuntimeConfigReasoningEffort_none: _('关闭', 'Off', '關閉'), + hermesAgentRuntimeConfigPersonalities: _('人格预设 JSON', 'Personalities JSON', '人格預設 JSON'), + hermesAgentRuntimeConfigFootnote: _('这些字段会写入 agent.*,影响 CLI 与 Gateway 长跑行为。将可选超时设为 0 表示关闭对应限制或通知;人格预设必须是名称到提示词的 JSON 映射;disabled_toolsets 和其他高级 agent 字段会保留在 raw YAML 中。', 'These fields are written under agent.* and affect CLI and Gateway long-running behavior. Set optional timeouts to 0 to disable the corresponding limit or notification. Personalities must be a JSON map from name to prompt. disabled_toolsets and other advanced agent fields stay in raw YAML.', '這些欄位會寫入 agent.*,影響 CLI 與 Gateway 長跑行為。將可選逾時設為 0 表示關閉對應限制或通知;人格預設必須是名稱到提示詞的 JSON 映射;disabled_toolsets 和其他進階 agent 欄位會保留在 raw YAML 中。'), hermesUnauthorizedDmConfigTitle: _('未授权私信', 'Unauthorized DMs', '未授權私訊'), hermesUnauthorizedDmConfigDesc: _('控制陌生用户直接私信 Hermes 时的全局响应策略,适合公网部署时减少无效打扰或保留配对入口。', 'Control the global response when unknown users send Hermes a direct message. Useful for public deployments that need fewer unsolicited replies or a pairing entry point.', '控制陌生使用者直接私訊 Hermes 時的全域回應策略,適合公開部署時減少無效打擾或保留配對入口。'), hermesUnauthorizedDmConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'), diff --git a/tests/hermes-agent-runtime-config.test.js b/tests/hermes-agent-runtime-config.test.js index abbc1e8..0461bea 100644 --- a/tests/hermes-agent-runtime-config.test.js +++ b/tests/hermes-agent-runtime-config.test.js @@ -19,6 +19,9 @@ test('Hermes Agent 长跑保护配置读取会提供上游默认值', () => { gatewayNotifyInterval: 180, gatewayAutoContinueFreshness: 3600, imageInputMode: 'auto', + agentVerbose: false, + reasoningEffort: 'medium', + personalitiesJson: '{}', }) }) @@ -34,6 +37,12 @@ test('Hermes Agent 长跑保护配置读取会回显 YAML 字段', () => { gateway_notify_interval: 240, gateway_auto_continue_freshness: 5400, image_input_mode: 'native', + verbose: true, + reasoning_effort: 'high', + personalities: { + concise: 'Keep answers short.', + teacher: 'Explain with examples.', + }, }, }) @@ -46,6 +55,12 @@ test('Hermes Agent 长跑保护配置读取会回显 YAML 字段', () => { assert.equal(values.gatewayNotifyInterval, 240) assert.equal(values.gatewayAutoContinueFreshness, 5400) assert.equal(values.imageInputMode, 'native') + assert.equal(values.agentVerbose, true) + assert.equal(values.reasoningEffort, 'high') + assert.deepEqual(JSON.parse(values.personalitiesJson), { + concise: 'Keep answers short.', + teacher: 'Explain with examples.', + }) }) test('Hermes Agent 长跑保护配置保存会保留未知字段并写入 agent', () => { @@ -67,6 +82,12 @@ test('Hermes Agent 长跑保护配置保存会保留未知字段并写入 agent' gatewayNotifyInterval: '120', gatewayAutoContinueFreshness: '1800', imageInputMode: 'text', + agentVerbose: true, + reasoningEffort: 'low', + personalitiesJson: JSON.stringify({ + concise: ' Keep replies brief. ', + ops: 'Focus on operational risk.', + }), }) assert.deepEqual(next.model, { provider: 'anthropic' }) @@ -80,10 +101,32 @@ test('Hermes Agent 长跑保护配置保存会保留未知字段并写入 agent' assert.equal(next.agent.gateway_notify_interval, 120) assert.equal(next.agent.gateway_auto_continue_freshness, 1800) assert.equal(next.agent.image_input_mode, 'text') + assert.equal(next.agent.verbose, true) + assert.equal(next.agent.reasoning_effort, 'low') + assert.deepEqual(next.agent.personalities, { + concise: 'Keep replies brief.', + ops: 'Focus on operational risk.', + }) assert.deepEqual(next.agent.disabled_toolsets, ['terminal']) assert.equal(next.agent.custom_flag, 'keep-agent') }) +test('Hermes Agent 长跑保护配置保存空人格会删除 personalities', () => { + const next = mergeHermesAgentRuntimeConfig({ + agent: { + personalities: { + concise: 'Keep answers short.', + }, + custom_flag: 'keep-agent', + }, + }, { + personalitiesJson: '{}', + }) + + assert.equal(next.agent.personalities, undefined) + assert.equal(next.agent.custom_flag, 'keep-agent') +}) + test('Hermes Agent 长跑保护配置保存允许 0 表示关闭或无限制', () => { const next = mergeHermesAgentRuntimeConfig({}, { gatewayTimeout: '0', @@ -117,4 +160,16 @@ test('Hermes Agent 长跑保护配置保存会拒绝非法枚举和越界值', ( () => mergeHermesAgentRuntimeConfig({}, { clarifyTimeout: '-1' }), /agent\.clarify_timeout/, ) + assert.throws( + () => mergeHermesAgentRuntimeConfig({}, { reasoningEffort: 'maximum' }), + /agent\.reasoning_effort/, + ) + assert.throws( + () => mergeHermesAgentRuntimeConfig({}, { personalitiesJson: '{"bad name":"x"}' }), + /agent\.personalities\.bad name/, + ) + assert.throws( + () => mergeHermesAgentRuntimeConfig({}, { personalitiesJson: '{"concise":123}' }), + /agent\.personalities\.concise/, + ) }) diff --git a/tests/hermes-config-page-ui.test.js b/tests/hermes-config-page-ui.test.js index 302dadd..eeb76e0 100644 --- a/tests/hermes-config-page-ui.test.js +++ b/tests/hermes-config-page-ui.test.js @@ -118,6 +118,9 @@ test('Hermes 配置页会暴露 Agent 长跑保护结构化配置字段', () => 'hm-agent-gateway-notify-interval', 'hm-agent-gateway-auto-continue-freshness', 'hm-agent-image-input-mode', + 'hm-agent-reasoning-effort', + 'hm-agent-verbose', + 'hm-agent-personalities-json', ]) { assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`) }