import { DEFAULT_AGENT_LEARNING } from "@opengeni/contracts"; import type { AgentLearningCategory, AgentLearningContext, AgentLearningMode, AgentLearningSettingsRecord, AgentLearningOverrides, } from "@opengeni/sdk"; import { useEffect, useId, useRef, useState } from "react"; import { Button } from "../../../ui/button"; import { ErrorMessage } from "../../../ui/error-message"; import { Select } from "../../../ui/select"; import { useAppContext } from "../../app-context"; import { LEARNING_MODE_LABEL } from "../../lib/agent-learning-vocabulary"; import { apiErrorAdvice, apiErrorDetails, isPermissionDenied, userErrorText, } from "../../../ui/api-error"; export { LEARNING_MODE_LABEL }; const UPDATE_PERMISSION_HELP = "Automatic applies agent changes right away. Review first waits for your OK in Knowledge › Review. Off stops agent changes."; function LearningModeSelect(props: { id: string; value: AgentLearningMode | "inherit"; defaultMode?: AgentLearningMode | undefined; allowInherit: boolean; compact?: boolean | undefined; describedBy?: string | undefined; onChange: (mode: AgentLearningMode | "inherit") => void; }) { // One vocabulary everywhere: Automatic, Review first, Off. An inherited // value says so, so an override is never mistaken for the default. const labels = LEARNING_MODE_LABEL; return ( ); } const CATEGORIES: { key: AgentLearningCategory; label: string; description: string }[] = [ { key: "knowledge", label: "Knowledge", description: "Retained sources, facts, decisions and useful findings.", }, { key: "instructions", label: "Instructions", description: "Standing guidance that shapes how agents work.", }, { key: "skills", label: "Skills", description: "Reusable procedures agents create or improve." }, ]; /** Shared editor: defaults and sparse chat/task overrides use the same authority. */ type AgentLearningSettingsEditorProps = { workspaceId: string; scope: "workspace" | "personal"; source?: AgentLearningContext | undefined; canEdit?: boolean | undefined; onSaved?: (() => void) | undefined; compact?: boolean | undefined; }; export function AgentLearningSettingsEditor(props: AgentLearningSettingsEditorProps) { const identity = JSON.stringify([ props.workspaceId, props.scope, props.source?.kind, props.source?.id, ]); return ; } /** * The last settings read per caller and scope. A chat's Agent tab opens * straight onto them and refreshes in place, instead of loading. */ const settingsCache = new WeakMap< object, Map >(); function settingsCacheKey(props: AgentLearningSettingsEditorProps) { return `${props.workspaceId}|${props.scope}|${props.source?.kind ?? ""}:${props.source?.id ?? ""}`; } /** Reads a chat's settings into the cache before its menu opens. Never throws. */ export function prefetchAgentLearningSettings( client: ReturnType["client"], props: AgentLearningSettingsEditorProps, ): void { const key = settingsCacheKey(props); if (settingsCache.get(client)?.has(key)) return; void Promise.all([ client.getAgentLearningSettings(props.workspaceId, props.scope, props.source), client.getAgentLearningSettings(props.workspaceId, props.scope), ]) .then(([record, defaults]) => { let byKey = settingsCache.get(client); if (!byKey) { byKey = new Map(); settingsCache.set(client, byKey); } if (!byKey.has(key)) byKey.set(key, { record, defaults }); }) .catch(() => undefined); } function AgentLearningSettingsFields(props: AgentLearningSettingsEditorProps) { const active = useRef(true); useEffect(() => { active.current = true; return () => { active.current = false; }; }, []); const context = useAppContext(); const fieldId = useId(); const cacheKey = settingsCacheKey(props); const cached = settingsCache.get(context.client)?.get(cacheKey) ?? null; const [record, setRecord] = useState( () => cached?.record ?? null, ); const [defaults, setDefaults] = useState( () => cached?.defaults ?? null, ); // A failed read and a failed save are different problems with different fixes. const [loadError, setLoadError] = useState(null); const [saveError, setSaveError] = useState(null); // Cached rows show while the read refreshes them, but a save waits for it: // it must go against the current version, not the cached one. const [refreshing, setRefreshing] = useState(true); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [reload, setReload] = useState(0); const sourceKind = props.source?.kind; const sourceId = props.source?.id; useEffect(() => { let current = true; if (!settingsCache.get(context.client)?.has(cacheKey)) setRecord(null); setLoadError(null); setSaveError(null); setRefreshing(true); setSaved(false); const source = sourceKind && sourceId ? { kind: sourceKind, id: sourceId } : undefined; void Promise.all([ context.client.getAgentLearningSettings(props.workspaceId, props.scope, source), context.client.getAgentLearningSettings(props.workspaceId, props.scope), ]) .then(([value, base]) => { let byKey = settingsCache.get(context.client); if (!byKey) { byKey = new Map(); settingsCache.set(context.client, byKey); } byKey.set(cacheKey, { record: value, defaults: base }); if (current) { setRecord(value); setDefaults(base); setRefreshing(false); } }) .catch((reason: unknown) => { if (current) { setLoadError(reason); setRefreshing(false); } }); return () => { current = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps -- cacheKey is derived from these }, [context.client, props.workspaceId, props.scope, sourceKind, sourceId, reload]); async function save(category: AgentLearningCategory, mode: AgentLearningMode | "inherit") { if (!record || saving || refreshing || props.canEdit === false) return; const invocation = context.captureWorkspaceInvocation(props.workspaceId); if (!invocation) return; setSaving(true); setSaved(false); setSaveError(null); try { const next = await context.client.saveAgentLearningSettings(props.workspaceId, { scope: props.scope, ...(props.source ? { source: props.source } : {}), operationId: crypto.randomUUID(), expectedVersion: record.version, settings: props.source ? { [category]: mode } : { ...record.settings, [category]: mode }, }); const entry = settingsCache.get(context.client)?.get(cacheKey); if (entry) entry.record = next; if (active.current && context.ownsWorkspaceInvocation(props.workspaceId, invocation)) { setRecord(next); setSaved(true); props.onSaved?.(); } } catch (reason) { if (active.current && context.ownsWorkspaceInvocation(props.workspaceId, invocation)) { setSaveError(reason); } } finally { if (active.current) setSaving(false); } } const retryLoad = () => setReload((n) => n + 1); if (!record) return loadError ? ( isPermissionDenied(loadError) ? (

You can't see Agent learning here. Ask a workspace admin for access.

) : ( Try again } {...apiErrorDetails(loadError)} > {apiErrorAdvice(loadError)} ) ) : ( ); return (
{!props.compact ? (

Automatic saves become available immediately. Review first saves a proposal and lets the agent continue. Off stops agent changes; existing knowledge and skills remain available.

) : (

{UPDATE_PERMISSION_HELP}

)}
Agent learning {CATEGORIES.map(({ key, label, description }) => (
{!props.compact ? (

{description}

) : null}
void save(key, mode)} />
))}
{props.compact ? (

Off stops agent changes. Agents still use what's already there.

) : null} {props.canEdit === false ? (

A workspace administrator can change these defaults.

) : null} {loadError ? ( // The rows are the last ones read; say they may be out of date. Try again ) } {...apiErrorDetails(loadError)} > {apiErrorAdvice(loadError)} ) : null} {saveError ? (

Couldn't save that. {userErrorText(saveError)}

) : null}

{saving ? "Saving…" : saved ? "Saved. Applies from the next agent run." : ""}

); } /** * The editor's rows while its settings load: the same rows and heights, so a * menu or page that opens onto it never jumps when they arrive. */ export function AgentLearningSkeleton({ compact }: { compact?: boolean | undefined }) { return (
{compact ?
); } /** Draft choices are committed atomically with creation, before a schedule can run. */ export function AgentLearningDraftEditor(props: { workspaceId: string; scope: "workspace" | "personal"; value: AgentLearningOverrides; onChange: (value: AgentLearningOverrides) => void; disabled?: boolean | undefined; compact?: boolean | undefined; }) { const { client } = useAppContext(); const id = useId(); const [defaults, setDefaults] = useState(null); const [error, setError] = useState(null); useEffect(() => { let current = true; setDefaults(null); setError(null); void client .getAgentLearningSettings(props.workspaceId, props.scope) .then((value) => { if (current) setDefaults(value); }) .catch((reason: unknown) => { if (current) setError(reason); }); return () => { current = false; }; }, [client, props.workspaceId, props.scope]); return (
Agent learning {!props.compact ? (

Override the defaults here. Review first saves proposals without pausing the agent.

) : (

{UPDATE_PERMISSION_HELP}

)}
{CATEGORIES.map(({ key, label }) => (
{ const next = { ...props.value }; if (value === "inherit") delete next[key]; else next[key] = value as AgentLearningMode; props.onChange(next); }} />
))}
{props.compact ? (

Off stops agent changes. Agents still use what's already there.

) : null} {error ? ( isPermissionDenied(error) ? (

The workspace defaults aren't visible to you. A workspace admin can see them.

) : (

Couldn't load the defaults. {userErrorText(error)}

) ) : null}
); }