blob: 5e0fa980ccd9360d9dbc633cf5e3085b1e02a464 [file]
import { isThinkingLevel, type ThinkingLevel } from './model-thinking.js';
export const SUBAGENT_PROFILES = ['local_read', 'web_research', 'implementation'] as const;
export type SubagentProfile = (typeof SUBAGENT_PROFILES)[number];
export const MAX_SUBAGENT_PRESETS = 64;
export const SUBAGENT_PRESET_ID_MAX_CHARS = 128;
// Normalization DROPS a preset whose name is too long and TRUNCATES a
// description that is, so any editor writing these fields has to enforce the
// same numbers — silently losing the preset the user just saved is the
// alternative. Exported for exactly that.
export const SUBAGENT_PRESET_NAME_MAX_CHARS = 128;
export const SUBAGENT_PRESET_DESCRIPTION_MAX_CHARS = 1_000;
const SAFE_SUBAGENT_PRESET_ID = /^[A-Za-z0-9._:-]+$/;
/** User-approved model route for one catalog subagent capability. */
export interface SubagentPreset {
id: string;
name: string;
description: string;
profile: SubagentProfile;
connectionSlug: string;
model: string;
thinkingLevel?: ThinkingLevel;
enabled: boolean;
}
export interface SubagentSettings {
presets: SubagentPreset[];
}
export function isSubagentProfile(value: unknown): value is SubagentProfile {
return typeof value === 'string' && (SUBAGENT_PROFILES as readonly string[]).includes(value);
}
export function isSafeSubagentPresetId(value: unknown): value is string {
return (
typeof value === 'string' &&
value.length > 0 &&
value.length <= SUBAGENT_PRESET_ID_MAX_CHARS &&
SAFE_SUBAGENT_PRESET_ID.test(value)
);
}
export function normalizeSubagentSettings(input: unknown): SubagentSettings {
if (
!input ||
typeof input !== 'object' ||
!Array.isArray((input as { presets?: unknown }).presets)
) {
return { presets: [] };
}
const presets: SubagentPreset[] = [];
const seen = new Set<string>();
for (const candidate of (input as { presets: unknown[] }).presets) {
if (presets.length >= MAX_SUBAGENT_PRESETS) break;
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) continue;
const value = candidate as Record<string, unknown>;
if (
!isSafeSubagentPresetId(value.id) ||
seen.has(value.id) ||
!isSubagentProfile(value.profile) ||
typeof value.name !== 'string' ||
value.name.trim().length < 1 ||
value.name.trim().length > SUBAGENT_PRESET_NAME_MAX_CHARS ||
typeof value.connectionSlug !== 'string' ||
value.connectionSlug.trim().length < 1 ||
value.connectionSlug.trim().length > 128 ||
typeof value.model !== 'string' ||
value.model.trim().length < 1 ||
value.model.trim().length > 512 ||
(value.enabled !== true && value.enabled !== false) ||
(value.thinkingLevel !== undefined && !isThinkingLevel(value.thinkingLevel))
) {
continue;
}
const id = value.id;
seen.add(id);
presets.push({
id,
name: value.name.trim(),
description:
typeof value.description === 'string'
? value.description.trim().slice(0, SUBAGENT_PRESET_DESCRIPTION_MAX_CHARS)
: '',
profile: value.profile,
connectionSlug: value.connectionSlug.trim(),
model: value.model.trim(),
...(value.thinkingLevel !== undefined ? { thinkingLevel: value.thinkingLevel } : {}),
enabled: value.enabled,
});
}
return { presets };
}