185 lines
6.9 KiB
TypeScript
185 lines
6.9 KiB
TypeScript
import { useTranslation } from 'react-i18next';
|
|
import { HelpText } from './HelpText';
|
|
import { FieldLabel, FieldInput } from './formUtils';
|
|
import type { SectionFormProps } from './types';
|
|
|
|
/**
|
|
* Reflection (Hermes mode) settings.
|
|
*
|
|
* Toggle + caps for the per-job reflection loop that auto-updates a user's
|
|
* persistent memory. See `src/engine/reflection/` and the design doc at
|
|
* docs/superpowers/specs/2026-05-11-self-improving-memory-design.md.
|
|
*
|
|
* For Reflection to actually run, a worker with `roles: [reflection]` must
|
|
* exist in the LLM Workers tab. Otherwise (with the default `worker_required`)
|
|
* jobs are silently skipped.
|
|
*/
|
|
export function ReflectionForm({ config, onChange }: SectionFormProps) {
|
|
const { t } = useTranslation('settings');
|
|
const reflection = config.reflection ?? {};
|
|
// Step 7 (design 2026-05-21): read v2 `llm.workers`. The v2 API contract
|
|
// already strips the legacy `provider` block from GET /api/config, so
|
|
// falling back to it would always be empty. Kept as a defensive `?? []`
|
|
// for the brief window where draft state may still be undefined.
|
|
const workers = config.llm?.workers ?? [];
|
|
const hasReflectionWorker = workers.some(
|
|
(w: { roles?: string[] }) => Array.isArray(w.roles) && w.roles.includes('reflection'),
|
|
);
|
|
const enabled = reflection.enabled === true;
|
|
|
|
return (
|
|
<div className="space-y-5">
|
|
<h2 className="text-base font-semibold text-slate-800">Reflection (Hermes mode)</h2>
|
|
|
|
<p className="text-xs text-slate-600 leading-relaxed">
|
|
{t('reflection.intro')}
|
|
</p>
|
|
|
|
<div>
|
|
<label className="flex items-center gap-2 text-sm text-slate-700">
|
|
<input
|
|
type="checkbox"
|
|
checked={enabled}
|
|
onChange={e => onChange('reflection.enabled', e.target.checked)}
|
|
className="rounded"
|
|
/>
|
|
<span className="font-semibold">{t('reflection.enableLabel')}</span>
|
|
</label>
|
|
<HelpText>{t('reflection.enableHelp')}</HelpText>
|
|
</div>
|
|
|
|
{enabled && !hasReflectionWorker && (
|
|
<div className="rounded-md border border-amber-300 dark:border-amber-500/30 bg-amber-50 dark:bg-amber-500/15 px-3 py-2 text-xs text-amber-900 dark:text-amber-300">
|
|
<div className="font-semibold mb-1">{t('reflection.workerWarn.title')}</div>
|
|
<div>{t('reflection.workerWarn.body')}</div>
|
|
<pre className="mt-2 text-2xs font-mono bg-canvas border border-amber-200 rounded p-2 overflow-auto">{t('reflection.workerWarn.snippet')}</pre>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<label className="flex items-center gap-2 text-sm text-slate-700">
|
|
<input
|
|
type="checkbox"
|
|
checked={reflection.workerRequired !== false}
|
|
onChange={e => onChange('reflection.workerRequired', e.target.checked)}
|
|
className="rounded"
|
|
/>
|
|
{t('reflection.workerRequiredLabel')}
|
|
</label>
|
|
<HelpText>{t('reflection.workerRequiredHelp')}</HelpText>
|
|
</div>
|
|
|
|
<h3 className="text-sm font-medium text-slate-600 mt-4 pt-3 border-t border-slate-200">Caps</h3>
|
|
|
|
<div>
|
|
<FieldLabel>Max memory changes per job</FieldLabel>
|
|
<FieldInput
|
|
type="number"
|
|
value={reflection.maxMemoryChangesPerJob ?? 3}
|
|
onChange={v => onChange('reflection.maxMemoryChangesPerJob', Number(v))}
|
|
/>
|
|
<HelpText>{t('reflection.maxMemHelp')}</HelpText>
|
|
</div>
|
|
|
|
<div>
|
|
<FieldLabel>Max entry body bytes</FieldLabel>
|
|
<FieldInput
|
|
type="number"
|
|
value={reflection.maxEntryBodyBytes ?? 8192}
|
|
onChange={v => onChange('reflection.maxEntryBodyBytes', Number(v))}
|
|
/>
|
|
<HelpText>{t('reflection.maxBodyHelp')}</HelpText>
|
|
</div>
|
|
|
|
<div>
|
|
<FieldLabel>Piece edit cooldown (hours)</FieldLabel>
|
|
<FieldInput
|
|
type="number"
|
|
value={reflection.pieceEditCooldownHours ?? 24}
|
|
onChange={v => onChange('reflection.pieceEditCooldownHours', Number(v))}
|
|
/>
|
|
<HelpText>{t('reflection.cooldownHelp')}</HelpText>
|
|
</div>
|
|
|
|
<div>
|
|
<FieldLabel>Activity log max bytes</FieldLabel>
|
|
<FieldInput
|
|
type="number"
|
|
value={reflection.activityLogMaxBytes ?? 4096}
|
|
onChange={v => onChange('reflection.activityLogMaxBytes', Number(v))}
|
|
/>
|
|
<HelpText>{t('reflection.activityLogHelp')}</HelpText>
|
|
</div>
|
|
|
|
<h3 className="text-sm font-medium text-slate-600 mt-4 pt-3 border-t border-slate-200">Budget</h3>
|
|
|
|
<div>
|
|
<FieldLabel>Per-user daily budget (tokens)</FieldLabel>
|
|
<FieldInput
|
|
type="number"
|
|
value={reflection.perUserDailyBudgetTokens ?? 200000}
|
|
onChange={v => onChange('reflection.perUserDailyBudgetTokens', Number(v))}
|
|
/>
|
|
<HelpText>{t('reflection.dailyBudgetHelp')}</HelpText>
|
|
</div>
|
|
|
|
<h3 className="text-sm font-medium text-slate-600 mt-4 pt-3 border-t border-slate-200">Snapshot & Retention</h3>
|
|
|
|
<div>
|
|
<FieldLabel>Snapshot retention (days)</FieldLabel>
|
|
<FieldInput
|
|
type="number"
|
|
value={reflection.snapshotRetentionDays ?? 90}
|
|
onChange={v => onChange('reflection.snapshotRetentionDays', Number(v))}
|
|
/>
|
|
<HelpText>{t('reflection.snapRetentionHelp')}</HelpText>
|
|
</div>
|
|
|
|
<div>
|
|
<FieldLabel>Snapshot max bytes per user</FieldLabel>
|
|
<FieldInput
|
|
type="number"
|
|
value={reflection.snapshotMaxBytesPerUser ?? 100 * 1024 * 1024}
|
|
onChange={v => onChange('reflection.snapshotMaxBytesPerUser', Number(v))}
|
|
/>
|
|
<HelpText>{t('reflection.snapMaxUserHelp')}</HelpText>
|
|
</div>
|
|
|
|
<div>
|
|
<FieldLabel>Snapshot max bytes per entry</FieldLabel>
|
|
<FieldInput
|
|
type="number"
|
|
value={reflection.snapshotMaxBytesPerEntry ?? 1 * 1024 * 1024}
|
|
onChange={v => onChange('reflection.snapshotMaxBytesPerEntry', Number(v))}
|
|
/>
|
|
<HelpText>{t('reflection.snapMaxEntryHelp')}</HelpText>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="flex items-center gap-2 text-sm text-slate-700">
|
|
<input
|
|
type="checkbox"
|
|
checked={reflection.storeLlmRaw === true}
|
|
onChange={e => onChange('reflection.storeLlmRaw', e.target.checked)}
|
|
className="rounded"
|
|
/>
|
|
{t('reflection.storeLlmRawLabel')}
|
|
</label>
|
|
<HelpText>{t('reflection.storeLlmRawHelp')}</HelpText>
|
|
</div>
|
|
|
|
<h3 className="text-sm font-medium text-slate-600 mt-4 pt-3 border-t border-slate-200">Monitoring</h3>
|
|
|
|
<div>
|
|
<FieldLabel>Abstain rate floor</FieldLabel>
|
|
<FieldInput
|
|
type="number"
|
|
value={reflection.abstainRateFloor ?? 0.3}
|
|
onChange={v => onChange('reflection.abstainRateFloor', v ? Number(v) : undefined)}
|
|
/>
|
|
<HelpText>{t('reflection.abstainFloorHelp')}</HelpText>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|