Add Calibrate AI persona presets with per-mode system prompts
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

This commit is contained in:
AetherForge
2026-06-07 02:31:14 -07:00
parent f89ba94cb7
commit 4ce9826660
12 changed files with 725 additions and 0 deletions

View File

@@ -3,6 +3,11 @@ import type { ServerSettings } from '../types';
import { api } from '../api/client';
import { HelpTip, FieldHint } from './HelpTip';
import { ADAPTIVE_STRATEGY_HELP } from '../help/lotlOnionTiers';
import {
AI_PERSONA_CHIPS,
DEFAULT_AI_PERSONA,
normalizeAIPersona,
} from '../help/aiPersonas';
export const DEFAULT_AI_LOCAL_ENDPOINT = 'http://127.0.0.1:11434/v1';
export const DEFAULT_AI_INTERVAL_SEC = 60;
@@ -25,6 +30,7 @@ export default function CalibrationAIControl({ server, onUpdate }: Props) {
const endpoint = readEndpoint(server);
const model = server.ai_model?.trim() || '';
const intervalSec = readIntervalSec(server);
const persona = normalizeAIPersona(server.ai_persona);
const [models, setModels] = useState<string[]>([]);
const [refreshing, setRefreshing] = useState(false);
@@ -87,6 +93,31 @@ export default function CalibrationAIControl({ server, onUpdate }: Props) {
<em>your</em> machines only. Complete fleet control stays on your LAN.
</p>
<div className="form-group">
<span className="label">AI persona</span>
<div
className="calibration-persona-chips"
role="group"
aria-label="AI persona preset"
>
{AI_PERSONA_CHIPS.map((chip) => (
<div key={chip.id} className="calibration-persona-chip-wrap">
<button
type="button"
className={`calibration-persona-chip${persona === chip.id ? ' calibration-persona-chip--selected' : ''}`}
style={{ ['--chip-accent' as string]: chip.color }}
aria-pressed={persona === chip.id}
onClick={() => onUpdate('server.ai_persona', chip.id)}
>
<span className="calibration-persona-chip-label">{chip.label}</span>
</button>
<HelpTip field={chip.helpField} />
</div>
))}
</div>
<FieldHint field="ai_persona" />
</div>
<div className="form-group">
<label htmlFor="cal-ai-endpoint" className="label">
Local API URL <HelpTip field="ai_local_endpoint" />

View File

@@ -0,0 +1,60 @@
/** Calibrate AI Control persona presets — distinct LLM system prompt modes. */
export type AIPersonaId = 'aggressive' | 'silent' | 'passive' | 'persuasive' | 'balanced';
export const AI_PERSONA_IDS: AIPersonaId[] = [
'aggressive',
'silent',
'passive',
'persuasive',
'balanced',
];
export const DEFAULT_AI_PERSONA: AIPersonaId = 'balanced';
export interface AIPersonaChip {
id: AIPersonaId;
label: string;
helpField: string;
color: string;
}
export const AI_PERSONA_CHIPS: AIPersonaChip[] = [
{
id: 'aggressive',
label: 'Aggressive',
helpField: 'ai_persona_aggressive',
color: '#ff4466',
},
{
id: 'silent',
label: 'Silent',
helpField: 'ai_persona_silent',
color: '#6b8cff',
},
{
id: 'passive',
label: 'Passive',
helpField: 'ai_persona_passive',
color: '#88ccaa',
},
{
id: 'persuasive',
label: 'Persuasive',
helpField: 'ai_persona_persuasive',
color: '#e8a028',
},
{
id: 'balanced',
label: 'Balanced',
helpField: 'ai_persona_balanced',
color: '#00e8f5',
},
];
export function normalizeAIPersona(value: string | undefined | null): AIPersonaId {
const v = (value ?? '').trim().toLowerCase();
if (AI_PERSONA_IDS.includes(v as AIPersonaId)) {
return v as AIPersonaId;
}
return DEFAULT_AI_PERSONA;
}

View File

@@ -19,6 +19,7 @@ const HELP_TIP_FIELDS = [
'fusion_media_mode', 'fusion_batch', 'fusion_run_order', 'fusion_output_name',
'obfuscate', 'sign_build', 'sigil_scramble', 'ai_enabled', 'ai_ollama_endpoint', 'ai_model',
'calibration_ai_control', 'ai_local_endpoint', 'calibration_ai_model', 'ai_no_context', 'ai_interval_sec',
'ai_persona', 'ai_persona_aggressive', 'ai_persona_silent', 'ai_persona_passive', 'ai_persona_persuasive', 'ai_persona_balanced',
'adaptive_strategy', 'lotl_onion_tiers',
'forge_operation_mode', 'forge_path_forge',
'mesh_p2p', 'auto_spread', 'hole_punch', 'remote_aggressive', 'usb_spread', 'share_spread',

View File

@@ -87,6 +87,12 @@ export const DOC_ANCHORS: Record<string, string> = {
calibration_ai_model: '/docs/#alerts-ai',
ai_no_context: '/docs/#alerts-ai',
ai_interval_sec: '/docs/#alerts-ai',
ai_persona: '/docs/#alerts-ai',
ai_persona_aggressive: '/docs/#alerts-ai',
ai_persona_silent: '/docs/#alerts-ai',
ai_persona_passive: '/docs/#alerts-ai',
ai_persona_persuasive: '/docs/#alerts-ai',
ai_persona_balanced: '/docs/#alerts-ai',
adaptive_strategy: '/docs/SPREAD_TECHNIQUES.html#lotl-tier-vuln_recon',
lotl_onion_tiers: '/docs/SPREAD_TECHNIQUES.html#lotl-tier-vuln_recon',

View File

@@ -101,6 +101,16 @@ describe('FIELD_HELP', () => {
'calibration_ai_model',
'ai_no_context',
'ai_interval_sec',
'ai_auto_elevate_clearance',
'fleet_phenotype',
'failure_atlas',
'ai_court_session',
'ai_persona',
'ai_persona_aggressive',
'ai_persona_silent',
'ai_persona_passive',
'ai_persona_persuasive',
'ai_persona_balanced',
'self_healing',
'firewall_exclusion',
'firewall_remote',

View File

@@ -32,6 +32,14 @@ export const FIELD_HELP: Record<string, string> = {
'Fleet adaptive strategy learns LOTL mining tier order from your own machines (OS, Docker/WSL probes, subnet, hashrate outcomes). On connect the server pushes a personalized tier walk with strategy_reasoning bullets before the agent tries the default onion. Overrides order/skip hints only — not wallet or patch_first gates. Toggle with server.adaptive_strategy_enabled (default on). When server.ai_control_enabled is on, Fleet AI Control replaces adaptive strategy for tier decisions.',
lotl_onion_tiers:
'Ordered spread contingency chain for LOTL Onion forges with lotl_policy_from_server. Mining tier order is separate (mining_tier_policy / adaptive_strategy). Spread tiers apply on reconnect without re-forge; adaptive strategy can reorder mining tiers proactively from fleet stats.',
ai_auto_elevate_clearance:
'When AI Control is on, the scheduler may raise an agent from L0L3 to L4 when the host is stuck with all spread tiers failed — so court-ordered restart_mining and reorder_tiers can execute. Events appear in LOTL Timeline clearance history and Access Depth badge flash.',
fleet_phenotype:
'When one agent in a fingerprint bucket finds a winning spread+mining path, the server publishes a fleet phenotype. Sibling agents inherit tier_order and spread_lane on auth without re-forge — Access Depth and LOTL Timeline show "cloned from" badges.',
failure_atlas:
'Server-side failure atlas records conditioned tier failures (e.g. ps_inmemory under Defender on). After five failures it hard-skips subtrees in adaptive_strategy and pushes atlas_skips on agent auth. LOTL Timeline marks tiers skipped_by_atlas.',
ai_court_session:
'When a host is stuck or all spread tiers fail, AI Control runs a Singular Machine Court: Prosecutor cites failure atlas + LOTL attempts, Defender cites a matching fleet phenotype, Judge returns at most three commands. Decisions persist with court_session=true on LOTL Timeline.',
calibration_ai_control:
'Calibrate control mode: Logic gates use weighted adaptive strategy + server lotl_onion_tiers. AI Control routes fleet decisions through a local LLM on this control PC every 60s per agent — stateless, no memory, full tool authority on your machines only.',
ai_local_endpoint:
@@ -42,6 +50,18 @@ export const FIELD_HELP: Record<string, string> = {
'Stateless AI mode — each 60s cycle sends only the current agent snapshot. No chat history or cross-agent memory is retained (always on for fleet safety).',
ai_interval_sec:
'Seconds between AI decision cycles per connected agent when AI Control is enabled. Default 60 — matches agent heartbeat cadence.',
ai_persona:
'Calibrate AI persona preset — shapes the LLM system prompt for fleet decisions. Aggressive maximizes spread+mine; Silent mines quietly; Passive observes; Persuasive spread-first; Balanced is default mission behavior.',
ai_persona_aggressive:
'Aggressive persona: take every spread and mine opportunity on your machines. Prioritize spread_now and discover_and_join each cycle, fast tier retries, and L4 clearance when stuck after tier exhaustion.',
ai_persona_silent:
'Silent persona: mine only with minimal spread. Never spread_now unless stuck after all 14 tiers; prefer container/wsl/docker. No shell agent_command unless clearance is high and hashrate stays zero.',
ai_persona_passive:
'Passive persona: observe and report mostly. Default noop unless hashrate=0 with exhausted chain (~10m stuck). Gentle restart_mining only — no aggressive spread or clearance elevation.',
ai_persona_persuasive:
'Persuasive persona: spread-first fleet growth. Attempt discover_and_join before restart_mining; probe join lanes and credential graph. Sell mining as fleet health after spread lanes succeed.',
ai_persona_balanced:
'Balanced persona: default mission prompt — balance spread vs mine, clearance-aware shell only when stuck, discover_and_join when join lanes are available.',
forge_path_forge:
'Server-side recursive batch seed: enter a folder path and the server walks it, placing a launcher next to every matching file without uploading anything. Lock Original renames the source so only the companion launcher can open it — it re-locks after playback.',
forge_recommended_defaults:

View File

@@ -1358,6 +1358,52 @@ button.deliverable-card .form-hint {
margin-top: 0.75rem;
}
.calibration-persona-chips {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.5rem 0 0.25rem;
}
.calibration-persona-chip-wrap {
display: inline-flex;
align-items: center;
gap: 0.2rem;
}
.calibration-persona-chip {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.45rem 0.85rem;
border-radius: 999px;
border: 1px solid var(--border-brass);
background: rgba(10, 10, 18, 0.65);
color: var(--text-secondary);
cursor: pointer;
font-family: var(--font-tech);
font-size: 0.82rem;
letter-spacing: 0.05em;
text-transform: uppercase;
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
}
.calibration-persona-chip:hover {
border-color: color-mix(in srgb, var(--chip-accent, var(--neon-cyan)) 55%, transparent);
background: rgba(0, 232, 245, 0.05);
}
.calibration-persona-chip--selected {
border-color: var(--chip-accent, var(--neon-magenta));
background: color-mix(in srgb, var(--chip-accent, var(--neon-magenta)) 14%, transparent);
color: var(--text-primary);
box-shadow: 0 0 18px color-mix(in srgb, var(--chip-accent, var(--neon-magenta)) 25%, transparent);
}
.calibration-persona-chip-label {
line-height: 1;
}
.calibration-ai-meta {
display: flex;
flex-wrap: wrap;

View File

@@ -218,6 +218,34 @@ describe('SettingsPage (Calibrate)', () => {
expect(saved?.server?.ai_no_context).toBe(true);
});
it('renders AI persona preset chips when AI Control is on', async () => {
const user = userEvent.setup();
renderSettings();
await screen.findByRole('button', { name: /AI Control/i });
await user.click(screen.getByRole('button', { name: /AI Control/i }));
const group = screen.getByRole('group', { name: 'AI persona preset' });
expect(within(group).getByRole('button', { name: 'Aggressive' })).toBeInTheDocument();
expect(within(group).getByRole('button', { name: 'Silent' })).toBeInTheDocument();
expect(within(group).getByRole('button', { name: 'Passive' })).toBeInTheDocument();
expect(within(group).getByRole('button', { name: 'Persuasive' })).toBeInTheDocument();
expect(within(group).getByRole('button', { name: 'Balanced' })).toHaveAttribute('aria-pressed', 'true');
});
it('saves selected ai_persona preset via updateConfig', async () => {
const user = userEvent.setup();
renderSettings();
await screen.findByRole('button', { name: /AI Control/i });
await user.click(screen.getByRole('button', { name: /AI Control/i }));
const group = screen.getByRole('group', { name: 'AI persona preset' });
await user.click(within(group).getByRole('button', { name: 'Aggressive' }));
await user.click(screen.getByRole('button', { name: /save calibration/i }));
await waitFor(() => {
expect(api.updateConfig).toHaveBeenCalled();
});
const saved = vi.mocked(api.updateConfig).mock.calls.at(-1)?.[0];
expect(saved?.server?.ai_persona).toBe('aggressive');
});
it('describes first-run admin credentials in Access Control help', async () => {
renderSettings();
expect(