dsh-llm-verifier 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,20 +1,25 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Button, IconDataOutline16, IconRefreshOutline16, Input, StateDot } from '@deepseek-ai/dsh-client-ui-primitives';
2
+ import { Button, IconDataOutline16, IconRefreshOutline16, Input } from '@deepseek-ai/dsh-client-ui-primitives';
3
3
  import { useEffect, useMemo, useState } from 'react';
4
4
  const NS = 'llm-verifier';
5
- const shell = { display: 'flex', flexDirection: 'column', gap: 18, padding: '8px 4px 32px', color: 'var(--dsw-text-primary)' };
6
- const card = { display: 'flex', flexDirection: 'column', gap: 0, padding: '16px 16px 0', border: '1px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.16))', borderRadius: 12, background: 'var(--dsw-alias-bg-module, rgba(20, 31, 57, 0.42))', overflow: 'hidden' };
7
- const sectionTitle = { display: 'flex', gap: 10, alignItems: 'center', padding: '0 0 12px', borderBottom: '1px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.16))' };
8
- const row = { display: 'grid', gridTemplateColumns: 'minmax(150px, 1fr) minmax(220px, 1.4fr)', gap: 18, alignItems: 'center', padding: '14px 0', borderBottom: '1px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.16))' };
9
- const selectStyle = { width: '100%', minHeight: 38, padding: '0 12px', borderRadius: 10, color: 'var(--dsw-text-primary)', background: 'var(--dsw-surface-sunken)', border: '1px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.16))' };
5
+ const shell = { width: '100%', maxWidth: 720, display: 'flex', flexDirection: 'column', gap: 12, padding: '0 0 32px', color: 'var(--dsw-alias-label-primary)' };
6
+ const settingsHeading = { margin: 0, fontSize: 16, fontWeight: 500, lineHeight: '24px', color: 'var(--dsw-alias-label-primary)' };
7
+ const settingsIntro = { margin: 0, fontSize: 14, lineHeight: '22px', color: 'var(--dsw-alias-label-tertiary)' };
8
+ const group = { width: '100%', display: 'flex', flexDirection: 'column', borderTop: '1px solid var(--dsw-alias-border-l2)' };
9
+ const groupTitle = { margin: 0, padding: '18px 0 8px', fontSize: 14, fontWeight: 500, lineHeight: '22px', color: 'var(--dsw-alias-label-primary)' };
10
+ const row = { minHeight: 64, display: 'grid', gridTemplateColumns: 'minmax(180px, 1fr) minmax(230px, 288px)', gap: 24, alignItems: 'center', padding: '12px 0', borderBottom: '1px solid var(--dsw-alias-border-l2)' };
11
+ const selectStyle = { boxSizing: 'border-box', width: '100%', height: 36, padding: '0 34px 0 12px', borderRadius: 8, color: 'var(--dsw-alias-label-primary)', background: 'var(--dsw-alias-bg-input)', border: '1px solid var(--dsw-alias-border-l2)', font: 'inherit', fontSize: 14, lineHeight: '22px', outline: 'none' };
12
+ const toggleStyle = (enabled) => ({ position: 'relative', justifySelf: 'end', width: 40, height: 22, padding: 0, border: 0, borderRadius: 999, cursor: 'pointer', transition: 'background .15s ease', background: enabled ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-bg-input)' });
13
+ const toggleThumbStyle = (enabled) => ({ position: 'absolute', top: 3, left: enabled ? 21 : 3, width: 16, height: 16, borderRadius: '50%', background: 'var(--dsw-static-neutral-00, #fff)', boxShadow: '0 1px 3px rgba(0,0,0,.28)', transition: 'left .15s ease' });
10
14
  const dashboardCard = { border: '1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.13))', background: 'color-mix(in srgb, var(--dsw-alias-bg-module, #171925) 88%, transparent)', borderRadius: 16, boxShadow: '0 12px 36px rgba(0,0,0,.12)' };
11
15
  const muted = { color: 'var(--dsw-text-secondary)', fontSize: 12 };
12
16
  const toolLabels = { verifier_compare: '两项对比', verifier_select: '多项优选', verifier_track: '进度跟踪', verifier_current_session: '会话验收' };
13
17
  const toolColors = { verifier_compare: '#4f8cff', verifier_select: '#8b6df6', verifier_track: '#2fc5c9', verifier_current_session: '#f5a524' };
14
18
  function record(value) { return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : {}; }
15
- function values(view) { const v = record(view.value); return { enabled: v.enabled !== false, provider: String(v.provider ?? ''), model: String(v.model ?? ''), ...(typeof v.reasoningEffort === 'string' ? { reasoningEffort: v.reasoningEffort } : {}), maxTokens: Number(v.maxTokens ?? 32768), maxConcurrency: Number(v.maxConcurrency ?? 8), maxRetries: Number(v.maxRetries ?? 3), timeoutMs: Number(v.timeoutMs ?? 300000), cacheMaxEntries: Number(v.cacheMaxEntries ?? 10000), estimatedInputUsdPerMillion: Number(v.estimatedInputUsdPerMillion ?? 0), estimatedOutputUsdPerMillion: Number(v.estimatedOutputUsdPerMillion ?? 0) }; }
19
+ function values(view) { const v = record(view.value); const mode = v.autoVerifyMode === 'manual' || v.autoVerifyMode === 'strict' ? v.autoVerifyMode : 'smart'; return { enabled: v.enabled !== false, autoVerifyMode: mode, autoVerifyThreshold: Number(v.autoVerifyThreshold ?? .65), autoVerifyRepeats: Number(v.autoVerifyRepeats ?? 1), autoVerifyMinToolCalls: Number(v.autoVerifyMinToolCalls ?? 3), autoVerifyMaxChars: Number(v.autoVerifyMaxChars ?? 80000), autoVerifyMaxPerTask: Number(v.autoVerifyMaxPerTask ?? 2), autoVerifyMaxPerSession: Number(v.autoVerifyMaxPerSession ?? 8), provider: String(v.provider ?? ''), model: String(v.model ?? ''), ...(typeof v.reasoningEffort === 'string' ? { reasoningEffort: v.reasoningEffort } : {}), maxTokens: Number(v.maxTokens ?? 32768), maxConcurrency: Number(v.maxConcurrency ?? 8), maxRetries: Number(v.maxRetries ?? 3), timeoutMs: Number(v.timeoutMs ?? 300000), cacheMaxEntries: Number(v.cacheMaxEntries ?? 10000), estimatedInputUsdPerMillion: Number(v.estimatedInputUsdPerMillion ?? 0), estimatedOutputUsdPerMillion: Number(v.estimatedOutputUsdPerMillion ?? 0) }; }
16
20
  function message(error) { return error instanceof Error ? error.message : String(error); }
17
- function Label({ title, help }) { return _jsxs("div", { children: [_jsx("div", { style: { fontWeight: 600 }, children: title }), _jsx("div", { style: { fontSize: 12, color: 'var(--dsw-text-secondary)', marginTop: 3 }, children: help })] }); }
21
+ function Label({ title, help }) { return _jsxs("div", { style: { minWidth: 0 }, children: [_jsx("div", { style: { fontSize: 14, fontWeight: 400, lineHeight: '22px', color: 'var(--dsw-alias-label-primary)' }, children: title }), _jsx("div", { style: { fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)', marginTop: 2 }, children: help })] }); }
22
+ function GroupTitle({ children }) { return _jsx("h3", { style: groupTitle, children: children }); }
18
23
  function compact(value) { return new Intl.NumberFormat('zh-CN', { notation: value >= 10_000 ? 'compact' : 'standard', maximumFractionDigits: value >= 1_000 ? 1 : 0 }).format(value); }
19
24
  function money(value) { return '$' + value.toLocaleString('en-US', { minimumFractionDigits: value < 0.01 ? 4 : 2, maximumFractionDigits: value < 0.01 ? 4 : 2 }); }
20
25
  function duration(value) { if (value < 1000)
@@ -69,9 +74,9 @@ function VerifierSettings({ api }) {
69
74
  setBusy(false);
70
75
  } };
71
76
  if (!loaded || !draft)
72
- return _jsxs("div", { style: shell, children: [_jsx("h2", { children: "LLM Verifier" }), _jsx("p", { children: error ?? '正在读取 DSH 模型和设置…' }), error && _jsx(Button, { onClick: () => void load(), children: "\u91CD\u8BD5" })] });
73
- const numeric = (key, min = 0) => _jsx(Input, { type: "number", min: min, value: String(draft[key]), onChange: e => patch(key, Number(e.target.value)) });
74
- return _jsxs("div", { style: shell, children: [_jsxs("div", { children: [_jsx("h2", { style: { margin: '0 0 6px' }, children: "LLM Verifier" }), _jsx("p", { style: { margin: 0, color: 'var(--dsw-text-secondary)' }, children: "\u9009\u62E9\u4EFB\u610F\u5DF2\u5728 DSH\u300C\u6A21\u578B\u300D\u9875\u914D\u7F6E\u5E76\u542F\u7528\u7684\u6A21\u578B\u4F5C\u4E3A\u72EC\u7ACB\u88C1\u5224\u3002\u8BBE\u7F6E\u5B9E\u65F6\u751F\u6548\u3002" })] }), _jsxs("div", { style: card, children: [_jsxs("div", { style: sectionTitle, children: [_jsx(StateDot, { state: draft.enabled ? 'done' : 'error' }), _jsx("strong", { children: "\u5DE5\u5177\u5F00\u5173" })] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u542F\u7528 Verifier \u5DE5\u5177", help: draft.enabled ? '四个 verifier 工具可被 Agent 调用,每次调用会向裁判模型发起请求。' : '已停用:Agent 调用 verifier 工具会立即返回错误,不产生任何模型请求。' }), _jsx("button", { onClick: () => patch('enabled', !draft.enabled), style: { width: 80, height: 32, borderRadius: 16, border: '1px solid var(--dsw-alias-border-l2, rgba(255,255,255,0.16))', background: draft.enabled ? 'var(--dsw-success, #2f9e5b)' : 'var(--dsw-surface-sunken)', color: '#fff', cursor: 'pointer', fontSize: 13, fontWeight: 600 }, children: draft.enabled ? '已启用' : '已停用' })] }), !draft.enabled && _jsx("div", { style: { padding: '0 0 14px', fontSize: 12, color: 'var(--dsw-state-warn-primary, #d9a441)' }, children: "\u26A0 \u5F53\u524D\u505C\u7528 verifier_compare / verifier_select / verifier_track / verifier_current_session \u56DB\u4E2A\u5DE5\u5177" })] }), _jsxs("div", { style: card, children: [_jsxs("div", { style: sectionTitle, children: [_jsx(StateDot, { state: "done" }), _jsx("strong", { children: "\u88C1\u5224\u6A21\u578B" })] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u4F9B\u5E94\u5546", help: "\u53EA\u663E\u793A\u5F53\u524D DSH \u4E2D\u53EF\u8DEF\u7531\u7684\u4F9B\u5E94\u5546" }), _jsx("select", { style: selectStyle, value: draft.provider, onChange: e => { const provider = e.target.value; const first = loaded.groups.find(g => g.id === provider)?.models[0]; setDraft({ ...draft, provider, ...(first ? { model: first.id, reasoningEffort: first.reasoning?.defaultEffort } : {}) }); }, children: loaded.groups.map(g => _jsxs("option", { value: g.id, children: [g.name, " \u00B7 ", g.id] }, g.id)) })] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u6A21\u578B", help: "\u6A21\u578B\u76EE\u5F55\u6765\u81EA DSH adapter\uFF0C\u9009\u62E9\u7ED3\u679C\u4F1A\u6301\u4E45\u5316" }), _jsx("select", { style: selectStyle, value: draft.model, onChange: e => { const model = e.target.value; const found = models.find(m => m.id === model); setDraft({ ...draft, model, ...(found?.reasoning?.defaultEffort ? { reasoningEffort: found.reasoning.defaultEffort } : { reasoningEffort: undefined }) }); }, children: models.map(m => _jsxs("option", { value: m.id, children: [m.name, " \u00B7 ", m.id] }, m.id)) })] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u63A8\u7406\u5F3A\u5EA6", help: "\u7531\u6240\u9009\u6A21\u578B adapter \u58F0\u660E\uFF1B\u7559\u7A7A\u4F7F\u7528\u6A21\u578B\u9ED8\u8BA4\u503C" }), _jsxs("select", { style: selectStyle, value: draft.reasoningEffort ?? '', onChange: e => patch('reasoningEffort', e.target.value || undefined), children: [_jsx("option", { value: "", children: "\u6A21\u578B\u9ED8\u8BA4" }), efforts.map(e => _jsx("option", { value: e.id, children: e.name }, e.id))] })] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u6700\u5927\u8F93\u51FA Token", help: "\u6BCF\u4E2A\u88C1\u5224\u8BF7\u6C42\u7684\u8F93\u51FA\u4E0A\u9650" }), numeric('maxTokens', 1)] })] }), _jsxs("div", { style: card, children: [_jsx("div", { style: sectionTitle, children: _jsx("strong", { children: "\u6267\u884C\u63A7\u5236" }) }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u6700\u5927\u5E76\u53D1", help: "\u6240\u6709 verifier \u5DE5\u5177\u5171\u4EAB\u7684\u8BF7\u6C42\u5E76\u53D1\u4E0A\u9650" }), numeric('maxConcurrency', 1)] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u6700\u591A\u91CD\u8BD5", help: "\u77ED\u6682\u7F51\u7EDC\u3001\u9650\u6D41\u548C\u670D\u52A1\u7AEF\u9519\u8BEF\u7684\u91CD\u8BD5\u6B21\u6570" }), numeric('maxRetries', 0)] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u8BF7\u6C42\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09", help: "\u5355\u4E2A\u6A21\u578B\u8BF7\u6C42\u7684\u8D85\u65F6\u65F6\u95F4" }), numeric('timeoutMs', 1)] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u7F13\u5B58\u6761\u76EE\u4E0A\u9650", help: "\u6301\u4E45\u8BC4\u5206\u7F13\u5B58\u4FDD\u7559\u7684\u6700\u5927\u6761\u76EE\u6570" }), numeric('cacheMaxEntries', 1)] })] }), _jsxs("div", { style: card, children: [_jsx("div", { style: sectionTitle, children: _jsx("strong", { children: "\u8D39\u7528\u4F30\u7B97\uFF08\u6BCF\u767E\u4E07 Token\uFF0CUSD\uFF09" }) }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u8F93\u5165\u4EF7\u683C", help: "\u4EC5\u7528\u4E8E\u7ED3\u679C\u4E2D\u7684 estimatedCostUsd" }), numeric('estimatedInputUsdPerMillion', 0)] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u8F93\u51FA\u4EF7\u683C", help: "\u4EC5\u7528\u4E8E\u7ED3\u679C\u4E2D\u7684 estimatedCostUsd" }), numeric('estimatedOutputUsdPerMillion', 0)] })] }), loaded.failures.length > 0 && _jsxs("div", { style: { ...card, borderColor: 'var(--dsw-alias-state-warn-primary, #d9a441)', paddingBottom: 16 }, children: [_jsx("strong", { children: "\u90E8\u5206\u6A21\u578B\u76EE\u5F55\u8BFB\u53D6\u5931\u8D25" }), loaded.failures.map(x => _jsx("div", { children: x }, x))] }), error && _jsx("div", { style: { color: 'var(--dsw-danger)' }, children: error }), saved && _jsx("div", { style: { color: 'var(--dsw-success)' }, children: "\u5DF2\u4FDD\u5B58\uFF0C\u540E\u7EED verifier \u8C03\u7528\u5C06\u4F7F\u7528\u65B0\u8BBE\u7F6E\u3002" }), _jsxs("div", { style: { display: 'flex', gap: 10 }, children: [_jsx(Button, { disabled: busy || !loaded.writable, onClick: () => void save(), children: busy ? '保存中…' : '保存设置' }), _jsx(Button, { variant: "outline", disabled: busy, onClick: () => void load(), children: "\u91CD\u65B0\u8F7D\u5165" })] })] });
77
+ return _jsxs("div", { style: shell, children: [_jsx("h2", { style: settingsHeading, children: "LLM Verifier" }), _jsx("p", { style: settingsIntro, children: error ?? '正在读取 DSH 模型和设置…' }), error && _jsx("div", { children: _jsx(Button, { variant: "outline", onClick: () => void load(), children: "\u91CD\u8BD5" }) })] });
78
+ const numeric = (key, min = 0) => _jsx(Input, { style: { width: '100%', height: 36, borderRadius: 8 }, type: "number", min: min, value: String(draft[key]), onChange: e => patch(key, Number(e.target.value)) });
79
+ return _jsxs("div", { style: shell, children: [_jsx("h2", { style: settingsHeading, children: "LLM Verifier" }), _jsx("p", { style: settingsIntro, children: "\u9009\u62E9\u5DF2\u5728 DSH\u300C\u6A21\u578B\u300D\u4E2D\u914D\u7F6E\u7684\u6A21\u578B\u4F5C\u4E3A\u72EC\u7ACB\u88C1\u5224\u3002\u4FEE\u6539\u540E\u70B9\u51FB\u9875\u9762\u5E95\u90E8\u7684\u4FDD\u5B58\u6309\u94AE\u751F\u6548\u3002" }), _jsxs("section", { style: group, children: [_jsx(GroupTitle, { children: "\u5DE5\u5177" }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u542F\u7528 Verifier \u5DE5\u5177", help: draft.enabled ? '允许 Agent 调用四个 verifier 工具并向裁判模型发起请求。' : '停用后,所有 verifier 工具都会立即返回错误。' }), _jsx("button", { type: "button", role: "switch", "aria-checked": draft.enabled, "aria-label": "\u542F\u7528 Verifier \u5DE5\u5177", onClick: () => patch('enabled', !draft.enabled), style: toggleStyle(draft.enabled), children: _jsx("span", { style: toggleThumbStyle(draft.enabled) }) })] }), !draft.enabled && _jsx("p", { style: { margin: '8px 0 0', fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-state-warn-label)' }, children: "verifier_compare\u3001verifier_select\u3001verifier_track \u548C verifier_current_session \u5F53\u524D\u4E0D\u53EF\u7528\u3002" })] }), _jsxs("section", { style: group, children: [_jsx(GroupTitle, { children: "\u81EA\u52A8\u9A8C\u6536" }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u8C03\u7528\u7B56\u7565", help: "\u624B\u52A8\u4EC5\u66B4\u9732\u5DE5\u5177\uFF1B\u667A\u80FD\u5728\u590D\u6742\u4E14\u6709\u771F\u5B9E\u8BC1\u636E\u7684\u4EFB\u52A1\u7ED3\u675F\u524D\u9A8C\u6536\uFF1B\u4E25\u683C\u5BF9\u4EFB\u4F55\u5DF2\u5B8C\u6210\u7684\u5199\u5165\u3001\u6267\u884C\u6216\u8FDC\u7A0B\u64CD\u4F5C\u8FDB\u884C\u9A8C\u6536\u3002" }), _jsxs("select", { style: selectStyle, value: draft.autoVerifyMode, onChange: e => patch('autoVerifyMode', e.target.value), children: [_jsx("option", { value: "manual", children: "\u624B\u52A8" }), _jsx("option", { value: "smart", children: "\u667A\u80FD\uFF08\u63A8\u8350\uFF09" }), _jsx("option", { value: "strict", children: "\u4E25\u683C" })] })] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u901A\u8FC7\u9608\u503C", help: "\u4F1A\u8BDD\u8BC1\u636E\u5206\u6570\u8FBE\u5230\u8BE5\u503C\u4E14\u80DC\u8FC7\u7A7A\u5DE5\u4F5C\u57FA\u7EBF\u624D\u5141\u8BB8\u7ED3\u675F\uFF1B\u8303\u56F4 0\u20131\u3002" }), numeric('autoVerifyThreshold', 0)] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u81EA\u52A8\u8BC4\u4F30\u8F6E\u6B21", help: "\u6BCF\u9879\u6807\u51C6\u7684\u81EA\u52A8\u8BC4\u5206\u91CD\u590D\u6B21\u6570\uFF1B1 \u4E3A\u4F4E\u6210\u672C\u521D\u7B5B\u3002" }), numeric('autoVerifyRepeats', 1)] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u667A\u80FD\u6A21\u5F0F\u6700\u5C11\u5DE5\u5177\u8C03\u7528", help: "\u8FBE\u5230\u6B64\u5DE5\u5177\u8BC1\u636E\u6570\u91CF\u4E14\u5305\u542B\u5199\u5165/\u6267\u884C\u7C7B\u64CD\u4F5C\u65F6\u624D\u81EA\u52A8\u9A8C\u6536\u3002" }), numeric('autoVerifyMinToolCalls', 1)] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u6700\u5927\u8BC1\u636E\u5B57\u7B26", help: "\u53D1\u9001\u7ED9\u88C1\u5224\u524D\u4FDD\u7559\u7684\u6700\u8FD1\u4F1A\u8BDD\u8BC1\u636E\u5B57\u7B26\u6570\u3002" }), numeric('autoVerifyMaxChars', 1000)] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u6BCF\u4EFB\u52A1\u6700\u591A\u9A8C\u6536", help: "\u4F4E\u5206\u53CD\u9988\u540E\u5141\u8BB8\u518D\u6B21\u9A8C\u6536\u7684\u6B21\u6570\u4E0A\u9650\uFF0C\u9632\u6B62\u5FAA\u73AF\u3002" }), numeric('autoVerifyMaxPerTask', 1)] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u6BCF\u4F1A\u8BDD\u6700\u591A\u9A8C\u6536", help: "\u540C\u4E00\u4F1A\u8BDD\u4E2D\u7684\u81EA\u52A8\u9A8C\u6536\u603B\u9884\u7B97\u3002" }), numeric('autoVerifyMaxPerSession', 1)] }), draft.autoVerifyMode !== 'manual' && _jsx("p", { style: { margin: '8px 0 0', fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-state-warn-label)' }, children: "\u81EA\u52A8\u9A8C\u6536\u4F1A\u5C06\u8131\u654F\u540E\u7684\u4EFB\u52A1\u3001Assistant \u8F68\u8FF9\u4E0E\u771F\u5B9E\u5DE5\u5177\u8F93\u51FA\u53D1\u9001\u7ED9\u6240\u9009\u88C1\u5224\u6A21\u578B\uFF1B\u672A\u901A\u8FC7\u65F6\u4F1A\u81EA\u52A8\u8981\u6C42 Agent \u4FEE\u590D\u5E76\u91CD\u65B0\u9A8C\u8BC1\u3002" })] }), _jsxs("section", { style: group, children: [_jsx(GroupTitle, { children: "\u88C1\u5224\u6A21\u578B" }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u4F9B\u5E94\u5546", help: "\u5F53\u524D DSH \u4E2D\u53EF\u8DEF\u7531\u7684\u6A21\u578B\u4F9B\u5E94\u5546\u3002" }), _jsx("select", { style: selectStyle, value: draft.provider, onChange: e => { const provider = e.target.value; const first = loaded.groups.find(g => g.id === provider)?.models[0]; setDraft({ ...draft, provider, ...(first ? { model: first.id, reasoningEffort: first.reasoning?.defaultEffort } : {}) }); }, children: loaded.groups.map(g => _jsxs("option", { value: g.id, children: [g.name, " \u00B7 ", g.id] }, g.id)) })] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u6A21\u578B", help: "\u7528\u4F5C\u88C1\u5224\u7684\u5177\u4F53\u6A21\u578B\u3002" }), _jsx("select", { style: selectStyle, value: draft.model, onChange: e => { const model = e.target.value; const found = models.find(m => m.id === model); setDraft({ ...draft, model, ...(found?.reasoning?.defaultEffort ? { reasoningEffort: found.reasoning.defaultEffort } : { reasoningEffort: undefined }) }); }, children: models.map(m => _jsxs("option", { value: m.id, children: [m.name, " \u00B7 ", m.id] }, m.id)) })] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u63A8\u7406\u5F3A\u5EA6", help: "\u7559\u7A7A\u65F6\u4F7F\u7528\u6240\u9009\u6A21\u578B\u7684\u9ED8\u8BA4\u503C\u3002" }), _jsxs("select", { style: selectStyle, value: draft.reasoningEffort ?? '', onChange: e => patch('reasoningEffort', e.target.value || undefined), children: [_jsx("option", { value: "", children: "\u6A21\u578B\u9ED8\u8BA4" }), efforts.map(e => _jsx("option", { value: e.id, children: e.name }, e.id))] })] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u6700\u5927\u8F93\u51FA Token", help: "\u5355\u6B21\u88C1\u5224\u8BF7\u6C42\u5141\u8BB8\u751F\u6210\u7684\u6700\u5927 Token \u6570\u3002" }), numeric('maxTokens', 1)] })] }), _jsxs("section", { style: group, children: [_jsx(GroupTitle, { children: "\u6267\u884C" }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u6700\u5927\u5E76\u53D1", help: "\u6240\u6709 verifier \u5DE5\u5177\u5171\u4EAB\u7684\u8BF7\u6C42\u5E76\u53D1\u4E0A\u9650\u3002" }), numeric('maxConcurrency', 1)] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u6700\u591A\u91CD\u8BD5", help: "\u9650\u6D41\u3001\u8D85\u65F6\u548C\u77ED\u6682\u7F51\u7EDC\u9519\u8BEF\u7684\u91CD\u8BD5\u6B21\u6570\u3002" }), numeric('maxRetries', 0)] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u8BF7\u6C42\u8D85\u65F6", help: "\u5355\u6B21\u6A21\u578B\u8BF7\u6C42\u7684\u8D85\u65F6\u65F6\u95F4\uFF0C\u5355\u4F4D\u4E3A\u6BEB\u79D2\u3002" }), numeric('timeoutMs', 1)] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u7F13\u5B58\u6761\u76EE\u4E0A\u9650", help: "\u672C\u5730\u6301\u4E45\u8BC4\u5206\u7F13\u5B58\u4FDD\u7559\u7684\u6700\u5927\u6761\u76EE\u6570\u3002" }), numeric('cacheMaxEntries', 1)] })] }), _jsxs("section", { style: group, children: [_jsx(GroupTitle, { children: "\u8D39\u7528\u4F30\u7B97" }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u8F93\u5165\u4EF7\u683C", help: "\u6BCF\u767E\u4E07\u8F93\u5165 Token \u7684\u7F8E\u5143\u4EF7\u683C\uFF0C\u4EC5\u7528\u4E8E\u7EDF\u8BA1\u4F30\u7B97\u3002" }), numeric('estimatedInputUsdPerMillion', 0)] }), _jsxs("div", { style: row, children: [_jsx(Label, { title: "\u8F93\u51FA\u4EF7\u683C", help: "\u6BCF\u767E\u4E07\u8F93\u51FA Token \u7684\u7F8E\u5143\u4EF7\u683C\uFF0C\u4EC5\u7528\u4E8E\u7EDF\u8BA1\u4F30\u7B97\u3002" }), numeric('estimatedOutputUsdPerMillion', 0)] })] }), loaded.failures.length > 0 && _jsxs("div", { style: { padding: '10px 12px', borderRadius: 8, background: 'var(--dsw-alias-state-warn-bg)', color: 'var(--dsw-alias-state-warn-label)', fontSize: 12, lineHeight: '18px' }, children: [_jsx("div", { style: { fontWeight: 500, marginBottom: 3 }, children: "\u90E8\u5206\u6A21\u578B\u76EE\u5F55\u8BFB\u53D6\u5931\u8D25" }), loaded.failures.map(x => _jsx("div", { children: x }, x))] }), error && _jsx("p", { style: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-state-error-primary)' }, children: error }), saved && _jsx("p", { style: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-state-success-primary)' }, children: "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\u3002" }), _jsxs("div", { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, paddingTop: 4 }, children: [_jsx(Button, { variant: "outline", disabled: busy, onClick: () => void load(), children: "\u91CD\u65B0\u8F7D\u5165" }), _jsx(Button, { variant: "primary", disabled: busy || !loaded.writable, onClick: () => void save(), children: busy ? '保存中…' : '保存设置' })] })] });
75
80
  }
76
81
  function Metric({ label, value, note, accent }) {
77
82
  return _jsxs("div", { style: { ...dashboardCard, padding: '16px 18px', minWidth: 0 }, children: [_jsx("div", { style: muted, children: label }), _jsx("div", { style: { fontSize: 25, fontWeight: 750, lineHeight: 1.2, margin: '7px 0 5px', color: accent }, children: value }), _jsx("div", { style: { ...muted, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: note })] });
@@ -1,8 +1,16 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  import z from 'schemastery';
3
3
  export declare const VERIFIER_SETTINGS_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace;
4
+ export type AutoVerifyMode = 'manual' | 'smart' | 'strict';
4
5
  export interface Config {
5
6
  enabled?: boolean;
7
+ autoVerifyMode?: AutoVerifyMode;
8
+ autoVerifyThreshold?: number;
9
+ autoVerifyRepeats?: number;
10
+ autoVerifyMinToolCalls?: number;
11
+ autoVerifyMaxChars?: number;
12
+ autoVerifyMaxPerTask?: number;
13
+ autoVerifyMaxPerSession?: number;
6
14
  provider?: string;
7
15
  model?: string;
8
16
  reasoningEffort?: string;
@@ -18,6 +26,13 @@ export interface Config {
18
26
  }
19
27
  export interface ResolvedConfig {
20
28
  enabled: boolean;
29
+ autoVerifyMode: AutoVerifyMode;
30
+ autoVerifyThreshold: number;
31
+ autoVerifyRepeats: number;
32
+ autoVerifyMinToolCalls: number;
33
+ autoVerifyMaxChars: number;
34
+ autoVerifyMaxPerTask: number;
35
+ autoVerifyMaxPerSession: number;
21
36
  provider: string;
22
37
  model: string;
23
38
  reasoningEffort?: string;
@@ -3,6 +3,13 @@ import z from 'schemastery';
3
3
  export const VERIFIER_SETTINGS_NAMESPACE = settingsNamespace('llm-verifier');
4
4
  export const Config = z.object({
5
5
  enabled: z.boolean().default(true),
6
+ autoVerifyMode: z.union(['manual', 'smart', 'strict']).default('smart'),
7
+ autoVerifyThreshold: z.number().min(0).max(1).default(0.65),
8
+ autoVerifyRepeats: z.number().step(1).min(1).default(1),
9
+ autoVerifyMinToolCalls: z.number().step(1).min(1).default(3),
10
+ autoVerifyMaxChars: z.number().step(1).min(1000).default(80000),
11
+ autoVerifyMaxPerTask: z.number().step(1).min(1).default(2),
12
+ autoVerifyMaxPerSession: z.number().step(1).min(1).default(8),
6
13
  provider: z.string().default('deepseek-official'),
7
14
  model: z.string().default('deepseek-v4-flash'),
8
15
  reasoningEffort: z.string(),
@@ -23,7 +30,18 @@ export function resolveConfig(config = {}) {
23
30
  throw new Error('llm-verifier: provider must be non-empty');
24
31
  if (!model)
25
32
  throw new Error('llm-verifier: model must be non-empty');
33
+ const autoVerifyMode = config.autoVerifyMode ?? 'smart';
34
+ if (!['manual', 'smart', 'strict'].includes(autoVerifyMode))
35
+ throw new Error('llm-verifier: autoVerifyMode must be manual, smart, or strict');
36
+ const autoVerifyThreshold = config.autoVerifyThreshold ?? 0.65;
37
+ if (!Number.isFinite(autoVerifyThreshold) || autoVerifyThreshold < 0 || autoVerifyThreshold > 1)
38
+ throw new Error('llm-verifier: autoVerifyThreshold must be between 0 and 1');
26
39
  const values = {
40
+ autoVerifyRepeats: config.autoVerifyRepeats ?? 1,
41
+ autoVerifyMinToolCalls: config.autoVerifyMinToolCalls ?? 3,
42
+ autoVerifyMaxChars: config.autoVerifyMaxChars ?? 80000,
43
+ autoVerifyMaxPerTask: config.autoVerifyMaxPerTask ?? 2,
44
+ autoVerifyMaxPerSession: config.autoVerifyMaxPerSession ?? 8,
27
45
  maxTokens: config.maxTokens ?? 32768,
28
46
  timeoutMs: config.timeoutMs ?? 300000,
29
47
  maxConcurrency: config.maxConcurrency ?? 8,
@@ -33,6 +51,8 @@ export function resolveConfig(config = {}) {
33
51
  for (const [name, value] of Object.entries(values))
34
52
  if (!Number.isSafeInteger(value) || value <= 0)
35
53
  throw new Error('llm-verifier: ' + name + ' must be a positive safe integer');
54
+ if (values.autoVerifyMaxChars < 1000)
55
+ throw new Error('llm-verifier: autoVerifyMaxChars must be at least 1000');
36
56
  const maxRetries = config.maxRetries ?? 3;
37
57
  if (!Number.isSafeInteger(maxRetries) || maxRetries < 0)
38
58
  throw new Error('llm-verifier: maxRetries must be a non-negative safe integer');
@@ -44,7 +64,7 @@ export function resolveConfig(config = {}) {
44
64
  if (![estimatedInputUsdPerMillion, estimatedOutputUsdPerMillion].every(value => Number.isFinite(value) && value >= 0))
45
65
  throw new Error('llm-verifier: estimated token prices must be finite non-negative numbers');
46
66
  const reasoningEffort = config.reasoningEffort?.trim();
47
- return { enabled: config.enabled ?? true, provider, model, ...(reasoningEffort ? { reasoningEffort } : {}), maxRetries, cacheDir, estimatedInputUsdPerMillion, estimatedOutputUsdPerMillion, ...values };
67
+ return { enabled: config.enabled ?? true, autoVerifyMode, autoVerifyThreshold, provider, model, ...(reasoningEffort ? { reasoningEffort } : {}), maxRetries, cacheDir, estimatedInputUsdPerMillion, estimatedOutputUsdPerMillion, ...values };
48
68
  }
49
69
  export function installVerifierSettings(ctx, entry, onChange) {
50
70
  let source = () => entry;
@@ -7,6 +7,7 @@ export * from './core.ts';
7
7
  export * from './engine.ts';
8
8
  export * from './cache.ts';
9
9
  export * from './statistics.ts';
10
+ export * from './auto.ts';
10
11
  export { callVerifier, RequestLimiter, type VerifierClientConfig, type VerifierImage, type UsageStats, type VerifierCompletion } from './caller.ts';
11
12
  export declare function apply(ctx: Context, config?: Config): void;
12
13
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools';
2
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
2
3
  import { Config, installVerifierSettings, resolveConfig } from "./config.js";
3
4
  import { RequestLimiter } from "./caller.js";
4
5
  import { TopLogprobCapabilityCache } from "./top-logprobs.js";
@@ -6,6 +7,7 @@ import { ScoreCache, resolveCacheFile } from "./cache.js";
6
7
  import { VerifierEngine, normalizeCriteria } from "./engine.js";
7
8
  import { loadVerifierImages } from "./images.js";
8
9
  import { extractSession } from "./session.js";
10
+ import { AutoVerificationBudget, analyzeAutoTask, automaticFeedback } from "./auto.js";
9
11
  import { StatisticsStore, emptyRunStats, errorDetails, resolveStatisticsFile } from "./statistics.js";
10
12
  export const name = 'llm-verifier';
11
13
  export const inject = ['tools', 'agents', 'attachments', 'llm', 'connection'];
@@ -14,6 +16,7 @@ export * from "./core.js";
14
16
  export * from "./engine.js";
15
17
  export * from "./cache.js";
16
18
  export * from "./statistics.js";
19
+ export * from "./auto.js";
17
20
  export { callVerifier, RequestLimiter } from "./caller.js";
18
21
  const criterionSchema = { type: 'object', additionalProperties: false, properties: { id: { type: 'string', required: true }, name: { type: 'string', required: true }, description: { type: 'string', required: true } } };
19
22
  const statsSchema = { type: 'object', additionalProperties: false, properties: { calls: { type: 'integer', required: true }, attempts: { type: 'integer', required: true }, retries: { type: 'integer', required: true }, inputTokens: { type: 'integer', required: true }, cachedInputTokens: { type: 'integer', required: true }, outputTokens: { type: 'integer', required: true }, reasoningTokens: { type: 'integer', required: true }, cacheHits: { type: 'integer', required: true }, cacheMisses: { type: 'integer', required: true }, estimatedCostUsd: { type: 'number', required: true }, topLogprobScores: { type: 'integer', required: true }, explicitTagScores: { type: 'integer', required: true } } };
@@ -29,6 +32,7 @@ function statsFrom(value) { if (typeof value !== 'object' || value === null || !
29
32
  function rpcSuccess(value) { return { ok: true, value }; }
30
33
  function rpcFailure(message) { return { ok: false, error: { code: 'bad-request', message, details: { issues: [] } } }; }
31
34
  export function apply(ctx, config = {}) {
35
+ const services = ctx;
32
36
  const entry = resolveConfig(config);
33
37
  let limiter = new RequestLimiter(entry.maxConcurrency);
34
38
  const current = installVerifierSettings(ctx, entry, () => { limiter = new RequestLimiter(current().maxConcurrency); });
@@ -36,32 +40,40 @@ export function apply(ctx, config = {}) {
36
40
  const cache = new ScoreCache(cacheFile, entry.cacheMaxEntries);
37
41
  const statistics = new StatisticsStore(resolveStatisticsFile(cacheFile));
38
42
  const topLogprobCapabilities = new TopLogprobCapabilityCache();
43
+ const autoBudget = new AutoVerificationBudget();
39
44
  const engine = async () => {
40
45
  const selected = current();
41
46
  await ctx.llm.resolveCallConfig({ provider: selected.provider, model: selected.model, ...(selected.reasoningEffort ? { reasoningEffort: selected.reasoningEffort } : {}), maxTokens: selected.maxTokens });
42
- return { verifier: new VerifierEngine({ ...selected, ctx, llm: ctx.llm, attachments: ctx.attachments, topLogprobCapabilities, limiter }, selected.maxConcurrency, cache, { input: selected.estimatedInputUsdPerMillion, output: selected.estimatedOutputUsdPerMillion }), selected };
47
+ return { verifier: new VerifierEngine({ ...selected, ctx, llm: ctx.llm, attachments: services.attachments, topLogprobCapabilities, limiter }, selected.maxConcurrency, cache, { input: selected.estimatedInputUsdPerMillion, output: selected.estimatedOutputUsdPerMillion }), selected };
43
48
  };
44
49
  const images = (values, signal) => loadVerifierImages(values, signal);
45
50
  const route = (selected) => ({ provider: selected.provider, model: selected.model });
46
51
  const requireEnabled = () => { if (!current().enabled)
47
52
  throw new Error('llm-verifier: verifier tools are disabled — enable them in Settings → LLM Verifier'); };
48
- const record = async (toolName, exec, operation) => {
53
+ const record = async (toolName, agent, operation) => {
49
54
  const startedAt = Date.now();
50
55
  let selected = current();
51
56
  try {
52
57
  const completed = await operation();
53
58
  selected = completed.selected;
54
59
  const value = { ...completed.result, ...route(selected) };
55
- await statistics.record({ toolName, ...(exec.agent ? { sessionId: String(exec.agent.id) } : {}), startedAt, success: true, provider: selected.provider, model: selected.model, stats: statsFrom(value) }).catch(() => { });
60
+ await statistics.record({ toolName, ...(agent ? { sessionId: String(agent.id) } : {}), startedAt, success: true, provider: selected.provider, model: selected.model, stats: statsFrom(value) }).catch(() => { });
56
61
  return value;
57
62
  }
58
63
  catch (error) {
59
64
  const details = errorDetails(error);
60
- await statistics.record({ toolName, ...(exec.agent ? { sessionId: String(exec.agent.id) } : {}), startedAt, success: false, ...details, provider: selected.provider, model: selected.model, stats: emptyRunStats() }).catch(() => { });
65
+ await statistics.record({ toolName, ...(agent ? { sessionId: String(agent.id) } : {}), startedAt, success: false, ...details, provider: selected.provider, model: selected.model, stats: emptyRunStats() }).catch(() => { });
61
66
  throw error;
62
67
  }
63
68
  };
64
- ctx.effect(() => ctx.connection.rpc.handle('/llm-verifier', async (endpoint, payload) => {
69
+ const verifySession = async (agent, options, signal) => record('verifier_current_session', agent, async () => {
70
+ const extracted = await extractSession(agent, async (ref) => { const stored = await services.attachments.readImage(ref, signal); return { data: stored.data, mediaType: stored.ref.mediaType }; }, { fromSeq: options.fromSeq, toSeq: options.toSeq, includeAssistantText: options.includeAssistantText, redactPatterns: options.redactPatterns, maxChars: options.maxChars });
71
+ const { verifier, selected } = await engine();
72
+ const compared = await verifier.compare({ problem: extracted.problem, candidateA: extracted.trace, candidateB: '(No useful work or verification was performed.)', repeats: positive(options.repeats, 2, 'repeats'), images: extracted.images }, signal);
73
+ const result = { sessionId: extracted.sessionId, problem: extracted.problem, score: compared.scoreA, baselineScore: compared.scoreB, winner: compared.winner, fromSeq: extracted.fromSeq, toSeq: extracted.toSeq, omittedCharacters: extracted.omittedCharacters, calls: compared.calls, stats: compared.stats };
74
+ return { result, selected };
75
+ });
76
+ ctx.effect(() => services.connection.rpc.handle('/llm-verifier', async (endpoint, payload) => {
65
77
  if (endpoint !== 'statistics')
66
78
  return rpcFailure('unknown llm-verifier endpoint');
67
79
  if (typeof payload !== 'object' || payload === null)
@@ -76,10 +88,30 @@ export function apply(ctx, config = {}) {
76
88
  return rpcFailure(error instanceof Error ? error.message : String(error));
77
89
  }
78
90
  }, { authority: 'loopback' }), 'llm-verifier: statistics rpc');
79
- ctx.tools.register(defineTool({ name: 'verifier_compare', description: 'Use autonomously when exactly two substantive answers, patches, plans, or execution trajectories need an independent evidence-based comparison and the choice is consequential or uncertain. Do not use for trivial deterministic questions or when there is only one candidate. Uses the verifier model selected in DSH Settings, with top-logprob A–T expectations when supported and explicit-tag fallback otherwise.', parameters: { problem: { type: 'string', required: true }, candidate_a: { type: 'string', required: true }, candidate_b: { type: 'string', required: true }, ...commonParams }, output: { schema: { type: 'object', additionalProperties: false, properties: { scoreA: { type: 'number', required: true }, scoreB: { type: 'number', required: true }, winner: { type: 'string', enum: ['A', 'B', 'tie'], required: true }, criteria: { type: 'array', items: criterionResultSchema, required: true }, calls: { type: 'integer', required: true }, stats: { ...statsSchema, required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true } } }, render: (_args, value) => renderJson(value) }, timeoutMs: entry.timeoutMs * 20, async execute(args, exec) { requireEnabled(); return record('verifier_compare', exec, async () => { const { verifier, selected } = await engine(); const result = await verifier.compare({ problem: args.problem, candidateA: args.candidate_a, candidateB: args.candidate_b, criteria: normalizeCriteria(args.criteria), repeats: positive(args.repeats, 2, 'repeats'), images: await images(args.images, exec.signal) }, exec.signal); return { result, selected }; }); } }));
80
- ctx.tools.register(defineTool({ name: 'verifier_select', description: 'Use autonomously when three or more substantive candidate answers, patches, plans, or trajectories must be ranked and an independent choice is valuable. Use verifier_compare for exactly two candidates; do not generate extra candidates merely to invoke this tool. Uses the configured DSH verifier model and the O(Nk) Probabilistic Pivot Tournament.', parameters: { problem: { type: 'string', required: true }, candidates: { type: 'array', items: { type: 'string' }, required: true }, ...commonParams, pivots: { type: 'integer' }, seed: { type: 'integer' } }, output: { schema: { type: 'object', additionalProperties: false, properties: { index: { type: 'integer', required: true }, best: { type: 'string', required: true }, scores: { type: 'array', items: { type: 'number' }, required: true }, ranking: { type: 'array', items: { type: 'integer' }, required: true }, pivots: { type: 'array', items: { type: 'integer' }, required: true }, comparisons: { type: 'integer', required: true }, calls: { type: 'integer', required: true }, stats: { ...statsSchema, required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true } } }, render: (_args, value) => renderJson(value) }, timeoutMs: entry.timeoutMs * 100, async execute(args, exec) { requireEnabled(); return record('verifier_select', exec, async () => { const { verifier, selected } = await engine(); const result = await verifier.select({ problem: args.problem, candidates: args.candidates, criteria: normalizeCriteria(args.criteria), repeats: positive(args.repeats, 2, 'repeats'), pivots: positive(args.pivots, 2, 'pivots'), seed: args.seed ?? 0, images: await images(args.images, exec.signal) }, exec.signal); return { result, selected }; }); } }));
81
- ctx.tools.register(defineTool({ name: 'verifier_track', description: 'Use autonomously for a genuinely multi-step task when progress at explicit checkpoints is uncertain or needs evidence-based measurement. Do not use for a single completed answer or invent checkpoints that were not supplied by the task history. Trusts observed output rather than narration.', parameters: { problem: { type: 'string', required: true }, steps: { type: 'array', items: { type: 'string' }, required: true }, checkpoints: { type: 'array', items: { type: 'integer' }, required: true }, repeats: commonParams.repeats, images: commonParams.images }, output: { schema: { type: 'object', additionalProperties: false, properties: { scores: { type: 'array', items: { type: 'number' }, required: true }, perRepeat: { type: 'array', items: { type: 'array', items: { type: 'number' } }, required: true }, calls: { type: 'integer', required: true }, stats: { ...statsSchema, required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true } } }, render: (_args, value) => renderJson(value) }, timeoutMs: entry.timeoutMs * 20, async execute(args, exec) { requireEnabled(); return record('verifier_track', exec, async () => { const { verifier, selected } = await engine(); const result = await verifier.track(args.problem, args.steps, args.checkpoints, positive(args.repeats, 2, 'repeats'), exec.signal, await images(args.images, exec.signal)); return { result, selected }; }); } }));
82
- ctx.tools.register(defineTool({ name: 'verifier_current_session', description: 'Use autonomously near the end of a non-trivial coding, debugging, migration, deployment, or operations task when independent completion verification would materially reduce risk and the session contains real tool evidence. Do not use for routine conversation, simple factual answers, or every turn. Extracts the current DSH session, applies secret redaction, bounds and truncation, then sends the evidence to the verifier model selected in Settings.', parameters: { from_seq: { type: 'integer' }, to_seq: { type: 'integer' }, include_assistant_text: { type: 'boolean' }, redact_patterns: { type: 'array', items: { type: 'string' } }, max_chars: { type: 'integer' }, repeats: { type: 'integer' } }, output: { schema: { type: 'object', additionalProperties: false, properties: { sessionId: { type: 'string', required: true }, problem: { type: 'string', required: true }, score: { type: 'number', required: true }, baselineScore: { type: 'number', required: true }, winner: { type: 'string', enum: ['A', 'B', 'tie'], required: true }, fromSeq: { type: 'integer', required: true }, toSeq: { type: 'integer', required: true }, omittedCharacters: { type: 'integer', required: true }, calls: { type: 'integer', required: true }, stats: { ...statsSchema, required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true } } }, render: (_args, value) => renderJson(value) }, timeoutMs: entry.timeoutMs * 20, async execute(args, exec) { requireEnabled(); return record('verifier_current_session', exec, async () => { const agent = exec.agent ?? ctx.agents.currentInitiator(); if (agent === undefined)
83
- throw new Error('llm-verifier: verifier_current_session requires an agent-owned tool call'); const extracted = await extractSession(agent, async (ref) => { const stored = await ctx.attachments.readImage(ref, exec.signal); return { data: stored.data, mediaType: stored.ref.mediaType }; }, { fromSeq: args.from_seq, toSeq: args.to_seq, includeAssistantText: args.include_assistant_text, redactPatterns: args.redact_patterns, maxChars: args.max_chars }); const { verifier, selected } = await engine(); const compared = await verifier.compare({ problem: extracted.problem, candidateA: extracted.trace, candidateB: '(No useful work or verification was performed.)', repeats: positive(args.repeats, 2, 'repeats'), images: extracted.images }, exec.signal); const result = { sessionId: extracted.sessionId, problem: extracted.problem, score: compared.scoreA, baselineScore: compared.scoreB, winner: compared.winner, fromSeq: extracted.fromSeq, toSeq: extracted.toSeq, omittedCharacters: extracted.omittedCharacters, calls: compared.calls, stats: compared.stats }; return { result, selected }; }); } }));
91
+ ctx.on('agent/disposed', ({ agent }) => { autoBudget.release(agent); });
92
+ ctx.on('agent/turn-stopping', async ({ agent, signal }) => {
93
+ const selected = current();
94
+ if (!selected.enabled || selected.autoVerifyMode === 'manual' || signal.aborted)
95
+ return;
96
+ const evidence = analyzeAutoTask(agent.session.events, { mode: selected.autoVerifyMode, minToolCalls: selected.autoVerifyMinToolCalls, maxPerTask: selected.autoVerifyMaxPerTask, maxPerSession: selected.autoVerifyMaxPerSession });
97
+ if (!autoBudget.claim(agent, evidence, { mode: selected.autoVerifyMode, minToolCalls: selected.autoVerifyMinToolCalls, maxPerTask: selected.autoVerifyMaxPerTask, maxPerSession: selected.autoVerifyMaxPerSession }))
98
+ return;
99
+ try {
100
+ const result = await verifySession(agent, { fromSeq: evidence.taskStartSeq, includeAssistantText: true, maxChars: selected.autoVerifyMaxChars, repeats: selected.autoVerifyRepeats }, signal);
101
+ const passed = result.winner === 'A' && result.score >= selected.autoVerifyThreshold;
102
+ if (!passed)
103
+ agent.steer(createUserMessage({ content: [{ type: 'text', text: automaticFeedback(result.score, result.baselineScore, result.winner, selected.autoVerifyThreshold) }], source: { kind: 'plugin', plugin: 'dsh-llm-verifier' } }));
104
+ }
105
+ catch (error) {
106
+ ctx.logger.warn('llm-verifier automatic session verification failed: ' + (error instanceof Error ? error.message : String(error)));
107
+ if (selected.autoVerifyMode === 'strict' && !signal.aborted)
108
+ agent.steer(createUserMessage({ content: [{ type: 'text', text: '[Automatic verifier gate]\nThe required strict verification could not run: ' + (error instanceof Error ? error.message : String(error)) + '\nInspect the task evidence and run a directly relevant verification command before concluding.' }], source: { kind: 'plugin', plugin: 'dsh-llm-verifier' } }));
109
+ }
110
+ });
111
+ ctx.tools.register(defineTool({ name: 'verifier_compare', description: 'Use autonomously when exactly two substantive answers, patches, plans, or execution trajectories need an independent evidence-based comparison and the choice is consequential or uncertain. Do not use for trivial deterministic questions or when there is only one candidate. Uses the verifier model selected in DSH Settings, with top-logprob A–T expectations when supported and explicit-tag fallback otherwise.', parameters: { problem: { type: 'string', required: true }, candidate_a: { type: 'string', required: true }, candidate_b: { type: 'string', required: true }, ...commonParams }, output: { schema: { type: 'object', additionalProperties: false, properties: { scoreA: { type: 'number', required: true }, scoreB: { type: 'number', required: true }, winner: { type: 'string', enum: ['A', 'B', 'tie'], required: true }, criteria: { type: 'array', items: criterionResultSchema, required: true }, calls: { type: 'integer', required: true }, stats: { ...statsSchema, required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true } } }, render: (_args, value) => renderJson(value) }, timeoutMs: entry.timeoutMs * 20, async execute(args, exec) { requireEnabled(); return record('verifier_compare', exec.agent, async () => { const { verifier, selected } = await engine(); const result = await verifier.compare({ problem: args.problem, candidateA: args.candidate_a, candidateB: args.candidate_b, criteria: normalizeCriteria(args.criteria), repeats: positive(args.repeats, 2, 'repeats'), images: await images(args.images, exec.signal) }, exec.signal); return { result, selected }; }); } }));
112
+ ctx.tools.register(defineTool({ name: 'verifier_select', description: 'Use autonomously when three or more substantive candidate answers, patches, plans, or trajectories must be ranked and an independent choice is valuable. Use verifier_compare for exactly two candidates; do not generate extra candidates merely to invoke this tool. Deterministic orchestrators should call this directly once they have three or more real candidates.', parameters: { problem: { type: 'string', required: true }, candidates: { type: 'array', items: { type: 'string' }, required: true }, ...commonParams, pivots: { type: 'integer' }, seed: { type: 'integer' } }, output: { schema: { type: 'object', additionalProperties: false, properties: { index: { type: 'integer', required: true }, best: { type: 'string', required: true }, scores: { type: 'array', items: { type: 'number' }, required: true }, ranking: { type: 'array', items: { type: 'integer' }, required: true }, pivots: { type: 'array', items: { type: 'integer' }, required: true }, comparisons: { type: 'integer', required: true }, calls: { type: 'integer', required: true }, stats: { ...statsSchema, required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true } } }, render: (_args, value) => renderJson(value) }, timeoutMs: entry.timeoutMs * 100, async execute(args, exec) { requireEnabled(); return record('verifier_select', exec.agent, async () => { const { verifier, selected } = await engine(); const result = await verifier.select({ problem: args.problem, candidates: args.candidates, criteria: normalizeCriteria(args.criteria), repeats: positive(args.repeats, 2, 'repeats'), pivots: positive(args.pivots, 2, 'pivots'), seed: args.seed ?? 0, images: await images(args.images, exec.signal) }, exec.signal); return { result, selected }; }); } }));
113
+ ctx.tools.register(defineTool({ name: 'verifier_track', description: 'Use autonomously for a genuinely multi-step task when progress at explicit checkpoints is uncertain or needs evidence-based measurement. Deterministic goal/workflow orchestrators should call this directly when real checkpoints already exist. Do not use for a single completed answer or invent checkpoints.', parameters: { problem: { type: 'string', required: true }, steps: { type: 'array', items: { type: 'string' }, required: true }, checkpoints: { type: 'array', items: { type: 'integer' }, required: true }, repeats: commonParams.repeats, images: commonParams.images }, output: { schema: { type: 'object', additionalProperties: false, properties: { scores: { type: 'array', items: { type: 'number' }, required: true }, perRepeat: { type: 'array', items: { type: 'array', items: { type: 'number' } }, required: true }, calls: { type: 'integer', required: true }, stats: { ...statsSchema, required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true } } }, render: (_args, value) => renderJson(value) }, timeoutMs: entry.timeoutMs * 20, async execute(args, exec) { requireEnabled(); return record('verifier_track', exec.agent, async () => { const { verifier, selected } = await engine(); const result = await verifier.track(args.problem, args.steps, args.checkpoints, positive(args.repeats, 2, 'repeats'), exec.signal, await images(args.images, exec.signal)); return { result, selected }; }); } }));
114
+ ctx.tools.register(defineTool({ name: 'verifier_current_session', description: 'Explicitly verify the current DSH session. Smart/strict policy can also invoke this gate automatically at the turn-stopping lifecycle boundary after consequential work with real tool evidence. Extracts the session, applies redaction and bounds, then sends the evidence to the configured verifier model.', parameters: { from_seq: { type: 'integer' }, to_seq: { type: 'integer' }, include_assistant_text: { type: 'boolean' }, redact_patterns: { type: 'array', items: { type: 'string' } }, max_chars: { type: 'integer' }, repeats: { type: 'integer' } }, output: { schema: { type: 'object', additionalProperties: false, properties: { sessionId: { type: 'string', required: true }, problem: { type: 'string', required: true }, score: { type: 'number', required: true }, baselineScore: { type: 'number', required: true }, winner: { type: 'string', enum: ['A', 'B', 'tie'], required: true }, fromSeq: { type: 'integer', required: true }, toSeq: { type: 'integer', required: true }, omittedCharacters: { type: 'integer', required: true }, calls: { type: 'integer', required: true }, stats: { ...statsSchema, required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true } } }, render: (_args, value) => renderJson(value) }, timeoutMs: entry.timeoutMs * 20, async execute(args, exec) { requireEnabled(); const agent = exec.agent ?? ctx.agents.currentInitiator(); if (agent === undefined)
115
+ throw new Error('llm-verifier: verifier_current_session requires an agent-owned tool call'); return verifySession(agent, { fromSeq: args.from_seq, toSeq: args.to_seq, includeAssistantText: args.include_assistant_text, redactPatterns: args.redact_patterns, maxChars: args.max_chars, repeats: args.repeats }, exec.signal); } }));
84
116
  }
85
117
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-llm-verifier",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Configurable DSH-native LLM verifier with a Web settings page",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,69 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { Session } from '@deepseek-ai/dsh-session'
3
+ import { createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
4
+ import { AutoVerificationBudget, analyzeAutoTask, automaticFeedback } from './auto.ts'
5
+
6
+ function taskSession() {
7
+ const session = Session.create('session-00000000-0000-4000-8000-000000000009' as never)
8
+ session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'Implement and test the feature' }], source: { kind: 'user' } }), { surfaceOp: 'append' })
9
+ return session
10
+ }
11
+
12
+ function call(session: ReturnType<typeof taskSession>, name: string, id: string) {
13
+ session.append('tool/call', { turn: 1, step: 1, callId: id as never, name, arguments: '{}' })
14
+ session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ callId: id as never, content: [{ type: 'text', text: 'ok' }], isError: false }) }, { surfaceOp: 'append' })
15
+ }
16
+
17
+ const smart = { mode: 'smart' as const, minToolCalls: 3, maxPerTask: 2, maxPerSession: 8 }
18
+
19
+ describe('automatic verification policy', () => {
20
+ it('requires consequential work and enough evidence in smart mode', () => {
21
+ const session = taskSession()
22
+ call(session, 'read', 'one')
23
+ call(session, 'grep', 'two')
24
+ expect(analyzeAutoTask(session.events, smart)).toMatchObject({ eligible: false, reason: 'no-consequential-work' })
25
+ call(session, 'edit', 'three')
26
+ expect(analyzeAutoTask(session.events, smart)).toMatchObject({ eligible: true, toolCalls: 3, consequentialToolCalls: 1 })
27
+ })
28
+
29
+ it('strict mode accepts one completed consequential call', () => {
30
+ const session = taskSession()
31
+ call(session, 'ssh_exec', 'one')
32
+ expect(analyzeAutoTask(session.events, { ...smart, mode: 'strict' })).toMatchObject({ eligible: true, reason: 'strict-eligible' })
33
+ })
34
+
35
+ it('manual verification suppresses the automatic gate', () => {
36
+ const session = taskSession()
37
+ call(session, 'edit', 'one')
38
+ call(session, 'pwsh', 'two')
39
+ call(session, 'verifier_current_session', 'three')
40
+ expect(analyzeAutoTask(session.events, smart)).toMatchObject({ eligible: false, reason: 'already-verified', hasManualSessionVerification: true })
41
+ })
42
+
43
+ it('resets per-task attempts for a new direct user message and enforces the session cap', () => {
44
+ const session = taskSession()
45
+ call(session, 'edit', 'one')
46
+ call(session, 'read', 'two')
47
+ call(session, 'pwsh', 'three')
48
+ const agent = { id: session.id, session } as never
49
+ const budget = new AutoVerificationBudget()
50
+ const policy = { ...smart, maxPerTask: 1, maxPerSession: 2 }
51
+ expect(budget.claim(agent, analyzeAutoTask(session.events, policy), policy)).toBe(true)
52
+ expect(budget.claim(agent, analyzeAutoTask(session.events, policy), policy)).toBe(false)
53
+ session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'Do another task' }], source: { kind: 'user' } }), { surfaceOp: 'append' })
54
+ call(session, 'write', 'four')
55
+ call(session, 'read', 'five')
56
+ call(session, 'pwsh', 'six')
57
+ expect(budget.claim(agent, analyzeAutoTask(session.events, policy), policy)).toBe(true)
58
+ session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'Third task' }], source: { kind: 'user' } }), { surfaceOp: 'append' })
59
+ call(session, 'edit', 'seven')
60
+ call(session, 'read', 'eight')
61
+ call(session, 'pwsh', 'nine')
62
+ expect(budget.claim(agent, analyzeAutoTask(session.events, policy), policy)).toBe(false)
63
+ })
64
+
65
+ it('builds actionable low-score feedback', () => {
66
+ expect(automaticFeedback(0.42, 0.31, 'A', 0.65)).toContain('42.0%')
67
+ expect(automaticFeedback(0.42, 0.31, 'A', 0.65)).toContain('verification command')
68
+ })
69
+ })
package/src/auto.ts ADDED
@@ -0,0 +1,113 @@
1
+ import type { Agent } from '@deepseek-ai/dsh-agent'
2
+ import type { SessionEvent } from '@deepseek-ai/dsh-session'
3
+
4
+ export type AutoVerifyMode = 'manual' | 'smart' | 'strict'
5
+
6
+ export interface AutoVerifyPolicy {
7
+ mode: AutoVerifyMode
8
+ minToolCalls: number
9
+ maxPerTask: number
10
+ maxPerSession: number
11
+ }
12
+
13
+ export interface AutoTaskEvidence {
14
+ taskStartSeq: number
15
+ toolCalls: number
16
+ completedToolResults: number
17
+ consequentialToolCalls: number
18
+ hasManualSessionVerification: boolean
19
+ eligible: boolean
20
+ reason: string
21
+ }
22
+
23
+ const PASSIVE_TOOLS = new Set([
24
+ 'read', 'read_image', 'glob', 'grep', 'web_search', 'ssh_list', 'job_list',
25
+ 'job_output', 'list_agents', 'get_goal', 'skill', 'mcp__codegraph__codegraph_explore',
26
+ ])
27
+ const VERIFIER_TOOLS = new Set([
28
+ 'verifier_compare', 'verifier_select', 'verifier_track', 'verifier_current_session',
29
+ ])
30
+ const CONSEQUENTIAL_TOOLS = new Set([
31
+ 'edit', 'write', 'pwsh', 'bash', 'run_code', 'codex_image_generate',
32
+ 'ssh_exec', 'ssh_upload', 'ssh_download', 'ssh_tunnel', 'ssh_cluster',
33
+ 'job_kill', 'workbench_session_delete', 'create_goal', 'update_goal',
34
+ ])
35
+
36
+ function latestDirectUserSeq(events: readonly SessionEvent[]): number | undefined {
37
+ for (let index = events.length - 1; index >= 0; index -= 1) {
38
+ const event = events[index]
39
+ if (event?.type === 'user/message' && event.data.source.kind === 'user') return event.seq
40
+ }
41
+ return undefined
42
+ }
43
+
44
+ function isConsequential(name: string): boolean {
45
+ if (CONSEQUENTIAL_TOOLS.has(name)) return true
46
+ if (PASSIVE_TOOLS.has(name) || VERIFIER_TOOLS.has(name)) return false
47
+ return /(?:edit|write|patch|apply|deploy|upload|delete|remove|kill|exec|shell|command|migration|database|tunnel|cluster)/iu.test(name)
48
+ }
49
+
50
+ export function analyzeAutoTask(events: readonly SessionEvent[], policy: AutoVerifyPolicy): AutoTaskEvidence {
51
+ const taskStartSeq = latestDirectUserSeq(events)
52
+ if (taskStartSeq === undefined) return { taskStartSeq: 0, toolCalls: 0, completedToolResults: 0, consequentialToolCalls: 0, hasManualSessionVerification: false, eligible: false, reason: 'no-direct-user-task' }
53
+
54
+ const relevant = events.filter(event => event.seq >= taskStartSeq)
55
+ const calls = relevant.filter((event): event is SessionEvent<'tool/call'> => event.type === 'tool/call')
56
+ const toolCalls = calls.filter(event => !VERIFIER_TOOLS.has(event.data.name)).length
57
+ const completedToolResults = relevant.filter(event => event.type === 'tool/result' && event.data.error === undefined && event.data.message.content.every(block => block.isError !== true)).length
58
+ const consequentialToolCalls = calls.filter(event => isConsequential(event.data.name)).length
59
+ const hasManualSessionVerification = calls.some(event => event.data.name === 'verifier_current_session')
60
+
61
+ if (policy.mode === 'manual') return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, eligible: false, reason: 'manual-mode' }
62
+ if (hasManualSessionVerification) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, eligible: false, reason: 'already-verified' }
63
+ if (consequentialToolCalls === 0) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, eligible: false, reason: 'no-consequential-work' }
64
+ if (completedToolResults === 0) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, eligible: false, reason: 'no-completed-evidence' }
65
+ if (policy.mode === 'smart' && toolCalls < policy.minToolCalls) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, eligible: false, reason: 'insufficient-tool-evidence' }
66
+ return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, eligible: true, reason: policy.mode + '-eligible' }
67
+ }
68
+
69
+ interface SessionBudget {
70
+ taskStartSeq: number
71
+ taskAttempts: number
72
+ sessionAttempts: number
73
+ lastEvaluatedSeq: number
74
+ }
75
+
76
+ export class AutoVerificationBudget {
77
+ private readonly states = new Map<string, SessionBudget>()
78
+
79
+ claim(agent: Agent, evidence: AutoTaskEvidence, policy: AutoVerifyPolicy): boolean {
80
+ if (!evidence.eligible) return false
81
+ const id = String(agent.id)
82
+ const previous = this.states.get(id)
83
+ const state: SessionBudget = previous ?? { taskStartSeq: evidence.taskStartSeq, taskAttempts: 0, sessionAttempts: 0, lastEvaluatedSeq: -1 }
84
+ if (state.taskStartSeq !== evidence.taskStartSeq) {
85
+ state.taskStartSeq = evidence.taskStartSeq
86
+ state.taskAttempts = 0
87
+ state.lastEvaluatedSeq = -1
88
+ }
89
+ const lastSeq = agent.session.events.at(-1)?.seq ?? -1
90
+ if (lastSeq <= state.lastEvaluatedSeq || state.taskAttempts >= policy.maxPerTask || state.sessionAttempts >= policy.maxPerSession) {
91
+ this.states.set(id, state)
92
+ return false
93
+ }
94
+ state.taskAttempts += 1
95
+ state.sessionAttempts += 1
96
+ state.lastEvaluatedSeq = lastSeq
97
+ this.states.set(id, state)
98
+ return true
99
+ }
100
+
101
+ release(agent: Agent): void {
102
+ this.states.delete(String(agent.id))
103
+ }
104
+ }
105
+
106
+ export function automaticFeedback(score: number, baselineScore: number, winner: 'A' | 'B' | 'tie', threshold: number): string {
107
+ const percent = (value: number) => (value * 100).toFixed(1) + '%'
108
+ return [
109
+ '[Automatic verifier gate]',
110
+ `The independent verifier did not clear this task for completion: evidence score ${percent(score)}, baseline ${percent(baselineScore)}, verdict ${winner}, required ${percent(threshold)}.`,
111
+ 'Re-open the task requirements, inspect the actual tool outputs for unresolved errors or missing proof, make any necessary corrections, and run a directly relevant verification command before concluding. Do not merely restate that the task is complete.',
112
+ ].join('\n')
113
+ }