mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-30 04:40:18 +08:00
feat(hermes): add tool loop guardrails form
This commit is contained in:
@@ -23,6 +23,17 @@ const COMPRESSION_DEFAULTS = {
|
||||
abortOnSummaryFailure: false,
|
||||
}
|
||||
|
||||
const TOOL_GUARDRAILS_DEFAULTS = {
|
||||
warningsEnabled: true,
|
||||
hardStopEnabled: false,
|
||||
warnExactFailure: 2,
|
||||
warnSameToolFailure: 3,
|
||||
warnNoProgress: 2,
|
||||
hardStopExactFailure: 5,
|
||||
hardStopSameToolFailure: 8,
|
||||
hardStopNoProgress: 5,
|
||||
}
|
||||
|
||||
const SESSION_RESET_MODES = ['both', 'idle', 'daily', 'none']
|
||||
|
||||
export function render() {
|
||||
@@ -32,15 +43,19 @@ export function render() {
|
||||
let yaml = ''
|
||||
let runtimeValues = { ...SESSION_RUNTIME_DEFAULTS }
|
||||
let compressionValues = { ...COMPRESSION_DEFAULTS }
|
||||
let toolGuardrailsValues = { ...TOOL_GUARDRAILS_DEFAULTS }
|
||||
let loading = true
|
||||
let runtimeLoading = true
|
||||
let compressionLoading = true
|
||||
let toolGuardrailsLoading = true
|
||||
let saving = false
|
||||
let runtimeSaving = false
|
||||
let compressionSaving = false
|
||||
let toolGuardrailsSaving = false
|
||||
let error = null
|
||||
let runtimeError = null
|
||||
let compressionError = null
|
||||
let toolGuardrailsError = null
|
||||
|
||||
function esc(value) {
|
||||
return String(value || '')
|
||||
@@ -51,7 +66,7 @@ export function render() {
|
||||
}
|
||||
|
||||
function isBusy() {
|
||||
return loading || runtimeLoading || compressionLoading || saving || runtimeSaving || compressionSaving
|
||||
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving
|
||||
}
|
||||
|
||||
function option(labelKey, value, selected) {
|
||||
@@ -68,7 +83,7 @@ export function render() {
|
||||
}
|
||||
|
||||
function renderRuntimePanel() {
|
||||
const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving
|
||||
const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || toolGuardrailsSaving
|
||||
return `
|
||||
<div class="hm-panel hm-config-runtime-panel">
|
||||
<div class="hm-panel-header">
|
||||
@@ -116,7 +131,7 @@ export function render() {
|
||||
}
|
||||
|
||||
function renderCompressionPanel() {
|
||||
const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving
|
||||
const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving || toolGuardrailsSaving
|
||||
return `
|
||||
<div class="hm-panel hm-config-runtime-panel hm-config-compression-panel">
|
||||
<div class="hm-panel-header">
|
||||
@@ -165,6 +180,68 @@ export function render() {
|
||||
`
|
||||
}
|
||||
|
||||
function renderToolGuardrailsPanel() {
|
||||
const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving
|
||||
return `
|
||||
<div class="hm-panel hm-config-runtime-panel hm-config-guardrails-panel">
|
||||
<div class="hm-panel-header">
|
||||
<div>
|
||||
<div class="hm-panel-title">${t('engine.hermesToolGuardrailsTitle')}</div>
|
||||
<div class="hm-channel-panel-desc">${t('engine.hermesToolGuardrailsDesc')}</div>
|
||||
</div>
|
||||
<div class="hm-panel-actions">
|
||||
<span class="hm-muted">${toolGuardrailsSaving ? t('engine.hermesConfigStatusSaving') : toolGuardrailsLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesToolGuardrailsStatusReady')}</span>
|
||||
<button class="hm-btn hm-btn--cta hm-btn--sm" id="hm-tool-guardrails-save" ${disabled ? 'disabled' : ''}>${t('engine.hermesToolGuardrailsSave')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hm-panel-body">
|
||||
${renderError(toolGuardrailsError)}
|
||||
<div class="hm-config-check-grid">
|
||||
<label class="hm-channel-check">
|
||||
<input id="hm-tool-guardrails-warnings-enabled" type="checkbox" ${toolGuardrailsValues.warningsEnabled ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
|
||||
<span>${t('engine.hermesToolGuardrailsWarningsEnabled')}</span>
|
||||
</label>
|
||||
<label class="hm-channel-check">
|
||||
<input id="hm-tool-guardrails-hard-stop-enabled" type="checkbox" ${toolGuardrailsValues.hardStopEnabled ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
|
||||
<span>${t('engine.hermesToolGuardrailsHardStopEnabled')}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="hm-config-subtitle">${t('engine.hermesToolGuardrailsWarnAfterTitle')}</div>
|
||||
<div class="hm-config-runtime-grid hm-config-guardrails-grid">
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesToolGuardrailsWarnExactFailure')}</span>
|
||||
<input id="hm-tool-guardrails-warn-exact-failure" class="hm-input" type="number" inputmode="numeric" min="1" max="100" step="1" value="${esc(toolGuardrailsValues.warnExactFailure)}" ${disabled ? 'disabled' : ''}>
|
||||
</label>
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesToolGuardrailsWarnSameToolFailure')}</span>
|
||||
<input id="hm-tool-guardrails-warn-same-tool-failure" class="hm-input" type="number" inputmode="numeric" min="1" max="100" step="1" value="${esc(toolGuardrailsValues.warnSameToolFailure)}" ${disabled ? 'disabled' : ''}>
|
||||
</label>
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesToolGuardrailsWarnNoProgress')}</span>
|
||||
<input id="hm-tool-guardrails-warn-no-progress" class="hm-input" type="number" inputmode="numeric" min="1" max="100" step="1" value="${esc(toolGuardrailsValues.warnNoProgress)}" ${disabled ? 'disabled' : ''}>
|
||||
</label>
|
||||
</div>
|
||||
<div class="hm-config-subtitle">${t('engine.hermesToolGuardrailsHardStopAfterTitle')}</div>
|
||||
<div class="hm-config-runtime-grid hm-config-guardrails-grid">
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesToolGuardrailsHardStopExactFailure')}</span>
|
||||
<input id="hm-tool-guardrails-hard-stop-exact-failure" class="hm-input" type="number" inputmode="numeric" min="1" max="100" step="1" value="${esc(toolGuardrailsValues.hardStopExactFailure)}" ${disabled ? 'disabled' : ''}>
|
||||
</label>
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesToolGuardrailsHardStopSameToolFailure')}</span>
|
||||
<input id="hm-tool-guardrails-hard-stop-same-tool-failure" class="hm-input" type="number" inputmode="numeric" min="1" max="100" step="1" value="${esc(toolGuardrailsValues.hardStopSameToolFailure)}" ${disabled ? 'disabled' : ''}>
|
||||
</label>
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesToolGuardrailsHardStopNoProgress')}</span>
|
||||
<input id="hm-tool-guardrails-hard-stop-no-progress" class="hm-input" type="number" inputmode="numeric" min="1" max="100" step="1" value="${esc(toolGuardrailsValues.hardStopNoProgress)}" ${disabled ? 'disabled' : ''}>
|
||||
</label>
|
||||
</div>
|
||||
<div class="hm-channel-footnote">${t('engine.hermesToolGuardrailsFootnote')}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function draw() {
|
||||
el.innerHTML = `
|
||||
<div class="hm-hero">
|
||||
@@ -181,6 +258,7 @@ export function render() {
|
||||
|
||||
${renderRuntimePanel()}
|
||||
${renderCompressionPanel()}
|
||||
${renderToolGuardrailsPanel()}
|
||||
|
||||
<div class="hm-panel">
|
||||
<div class="hm-panel-header">
|
||||
@@ -202,6 +280,7 @@ export function render() {
|
||||
el.querySelector('#hm-config-save')?.addEventListener('click', save)
|
||||
el.querySelector('#hm-runtime-save')?.addEventListener('click', saveRuntime)
|
||||
el.querySelector('#hm-compression-save')?.addEventListener('click', saveCompression)
|
||||
el.querySelector('#hm-tool-guardrails-save')?.addEventListener('click', saveToolGuardrails)
|
||||
}
|
||||
|
||||
async function loadRaw() {
|
||||
@@ -219,13 +298,20 @@ export function render() {
|
||||
compressionValues = { ...COMPRESSION_DEFAULTS, ...(data?.values || {}) }
|
||||
}
|
||||
|
||||
async function loadToolGuardrails() {
|
||||
const data = await api.hermesToolLoopGuardrailsConfigRead()
|
||||
toolGuardrailsValues = { ...TOOL_GUARDRAILS_DEFAULTS, ...(data?.values || {}) }
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading = true
|
||||
runtimeLoading = true
|
||||
compressionLoading = true
|
||||
toolGuardrailsLoading = true
|
||||
error = null
|
||||
runtimeError = null
|
||||
compressionError = null
|
||||
toolGuardrailsError = null
|
||||
draw()
|
||||
try {
|
||||
await loadRaw()
|
||||
@@ -250,6 +336,14 @@ export function render() {
|
||||
compressionLoading = false
|
||||
draw()
|
||||
}
|
||||
try {
|
||||
await loadToolGuardrails()
|
||||
} catch (err) {
|
||||
toolGuardrailsError = humanizeError(err, t('engine.hermesToolGuardrailsLoadFailed') || 'Load tool guardrail config failed')
|
||||
} finally {
|
||||
toolGuardrailsLoading = false
|
||||
draw()
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshRawAfterStructuredSave() {
|
||||
@@ -277,6 +371,9 @@ export function render() {
|
||||
try {
|
||||
await loadCompression()
|
||||
} catch {}
|
||||
try {
|
||||
await loadToolGuardrails()
|
||||
} catch {}
|
||||
} catch (err) {
|
||||
error = humanizeError(err, t('engine.hermesConfigSaveFailed') || 'Save failed')
|
||||
toast(error, 'error')
|
||||
@@ -345,6 +442,38 @@ export function render() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveToolGuardrails() {
|
||||
const form = {
|
||||
warningsEnabled: !!el.querySelector('#hm-tool-guardrails-warnings-enabled')?.checked,
|
||||
hardStopEnabled: !!el.querySelector('#hm-tool-guardrails-hard-stop-enabled')?.checked,
|
||||
warnExactFailure: el.querySelector('#hm-tool-guardrails-warn-exact-failure')?.value || '2',
|
||||
warnSameToolFailure: el.querySelector('#hm-tool-guardrails-warn-same-tool-failure')?.value || '3',
|
||||
warnNoProgress: el.querySelector('#hm-tool-guardrails-warn-no-progress')?.value || '2',
|
||||
hardStopExactFailure: el.querySelector('#hm-tool-guardrails-hard-stop-exact-failure')?.value || '5',
|
||||
hardStopSameToolFailure: el.querySelector('#hm-tool-guardrails-hard-stop-same-tool-failure')?.value || '8',
|
||||
hardStopNoProgress: el.querySelector('#hm-tool-guardrails-hard-stop-no-progress')?.value || '5',
|
||||
}
|
||||
toolGuardrailsSaving = true
|
||||
toolGuardrailsError = null
|
||||
draw()
|
||||
try {
|
||||
const result = await api.hermesToolLoopGuardrailsConfigSave(form)
|
||||
toolGuardrailsValues = { ...TOOL_GUARDRAILS_DEFAULTS, ...(result?.values || form) }
|
||||
await refreshRawAfterStructuredSave()
|
||||
const backup = result?.backup || ''
|
||||
toast({
|
||||
message: t('engine.hermesToolGuardrailsSaveSuccess'),
|
||||
hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '',
|
||||
}, 'success')
|
||||
} catch (err) {
|
||||
toolGuardrailsError = humanizeError(err, t('engine.hermesToolGuardrailsSaveFailed') || 'Save tool guardrail config failed')
|
||||
toast(toolGuardrailsError, 'error')
|
||||
} finally {
|
||||
toolGuardrailsSaving = false
|
||||
draw()
|
||||
}
|
||||
}
|
||||
|
||||
draw()
|
||||
load()
|
||||
return el
|
||||
|
||||
@@ -6757,6 +6757,17 @@ body[data-active-engine="hermes"][data-theme="dark"] {
|
||||
grid-template-columns: repeat(4, minmax(140px, 1fr));
|
||||
margin-top: 18px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-config-guardrails-grid {
|
||||
grid-template-columns: repeat(3, minmax(160px, 1fr));
|
||||
margin-top: 12px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-config-subtitle {
|
||||
margin-top: 20px;
|
||||
color: var(--hm-text-secondary);
|
||||
font-family: var(--hm-font-serif);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
[data-engine="hermes"] .hm-config-check-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -6881,6 +6892,7 @@ body[data-active-engine="hermes"][data-theme="dark"] {
|
||||
}
|
||||
[data-engine="hermes"] .hm-config-runtime-grid,
|
||||
[data-engine="hermes"] .hm-config-compression-grid,
|
||||
[data-engine="hermes"] .hm-config-guardrails-grid,
|
||||
[data-engine="hermes"] .hm-config-check-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -513,6 +513,8 @@ export const api = {
|
||||
hermesSessionRuntimeConfigSave: (form) => invoke('hermes_session_runtime_config_save', { form }),
|
||||
hermesCompressionConfigRead: () => invoke('hermes_compression_config_read'),
|
||||
hermesCompressionConfigSave: (form) => invoke('hermes_compression_config_save', { form }),
|
||||
hermesToolLoopGuardrailsConfigRead: () => invoke('hermes_tool_loop_guardrails_config_read'),
|
||||
hermesToolLoopGuardrailsConfigSave: (form) => invoke('hermes_tool_loop_guardrails_config_save', { form }),
|
||||
hermesLazyDepsFeatures: () => cachedInvoke('hermes_lazy_deps_features', {}, 600000),
|
||||
hermesLazyDepsStatus: (features) => invoke('hermes_lazy_deps_status', { features }),
|
||||
hermesLazyDepsEnsure: (feature) => invoke('hermes_lazy_deps_ensure', { feature }),
|
||||
|
||||
@@ -512,6 +512,24 @@ export default {
|
||||
hermesCompressionProtectFirstN: _('保护开头消息数', 'Protect first messages', '保護開頭訊息數'),
|
||||
hermesCompressionAbortOnSummaryFailure: _('摘要失败时中止回复', 'Abort when summarization fails', '摘要失敗時中止回覆'),
|
||||
hermesCompressionFootnote: _('阈值和目标比例越低,压缩越早、越激进。建议先使用默认值,再根据真实 Gateway 日志调整。', 'Lower thresholds and target ratios compress earlier and more aggressively. Start with the defaults, then tune with real Gateway logs.', '閾值和目標比例越低,壓縮越早、越激進。建議先使用預設值,再根據真實 Gateway 日誌調整。'),
|
||||
hermesToolGuardrailsTitle: _('工具循环防护', 'Tool loop guardrails', '工具循環防護'),
|
||||
hermesToolGuardrailsDesc: _('当 Agent 重复失败或反复执行无进展工具时,先给模型修正提示;开启硬停止后可主动中止失控循环。', 'Warn the model when tools repeat failures or make no progress. Enable hard stops to halt runaway loops before they spend the full turn budget.', '當 Agent 重複失敗或反覆執行無進展工具時,先給模型修正提示;啟用硬停止後可主動中止失控循環。'),
|
||||
hermesToolGuardrailsStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
|
||||
hermesToolGuardrailsSave: _('保存防护配置', 'Save guardrail settings', '儲存防護設定'),
|
||||
hermesToolGuardrailsSaveSuccess: _('工具循环防护已保存,建议重启 Hermes Gateway 生效', 'Tool loop guardrails saved. Restart Hermes Gateway to take effect.', '工具循環防護已儲存,建議重啟 Hermes Gateway 生效'),
|
||||
hermesToolGuardrailsLoadFailed: _('加载工具循环防护失败', 'Load tool loop guardrails failed', '載入工具循環防護失敗'),
|
||||
hermesToolGuardrailsSaveFailed: _('保存工具循环防护失败', 'Save tool loop guardrails failed', '儲存工具循環防護失敗'),
|
||||
hermesToolGuardrailsWarningsEnabled: _('启用软警告', 'Enable soft warnings', '啟用軟警告'),
|
||||
hermesToolGuardrailsHardStopEnabled: _('启用硬停止', 'Enable hard stops', '啟用硬停止'),
|
||||
hermesToolGuardrailsWarnAfterTitle: _('软警告阈值', 'Soft warning thresholds', '軟警告閾值'),
|
||||
hermesToolGuardrailsHardStopAfterTitle: _('硬停止阈值', 'Hard stop thresholds', '硬停止閾值'),
|
||||
hermesToolGuardrailsWarnExactFailure: _('相同失败警告', 'Exact failure warning', '相同失敗警告'),
|
||||
hermesToolGuardrailsWarnSameToolFailure: _('同工具失败警告', 'Same-tool failure warning', '同工具失敗警告'),
|
||||
hermesToolGuardrailsWarnNoProgress: _('无进展警告', 'No-progress warning', '無進展警告'),
|
||||
hermesToolGuardrailsHardStopExactFailure: _('相同失败停止', 'Exact failure stop', '相同失敗停止'),
|
||||
hermesToolGuardrailsHardStopSameToolFailure: _('同工具失败停止', 'Same-tool failure stop', '同工具失敗停止'),
|
||||
hermesToolGuardrailsHardStopNoProgress: _('无进展停止', 'No-progress stop', '無進展停止'),
|
||||
hermesToolGuardrailsFootnote: _('默认只提示不拦截,适合交互式使用。硬停止更适合 cron、无人值守和长时间后台任务。', 'By default Hermes only warns and does not block, which fits interactive use. Hard stops are better for cron, unattended, and long-running background jobs.', '預設只提示不攔截,適合互動式使用。硬停止更適合 cron、無人值守和長時間背景任務。'),
|
||||
// Batch 1 §E: 会话导出
|
||||
sessionsExport: _('导出', 'Export', '匯出'),
|
||||
sessionsExportSuccess: _('已导出', 'Exported', '已匯出'),
|
||||
|
||||
Reference in New Issue
Block a user