diff --git a/.changeset/compaction-threshold-toggle.md b/.changeset/compaction-threshold-toggle.md new file mode 100644 index 000000000..d5233c97e --- /dev/null +++ b/.changeset/compaction-threshold-toggle.md @@ -0,0 +1,5 @@ +--- +'@truefoundry/trueforge-ui': patch +--- + +Make the compaction threshold an Auto/Custom selector: Auto omits `trigger` (runtime derives ~80% of the model context window); Custom reveals a number input defaulting to 50000. diff --git a/packages/trueforge-ui/src/atoms/draft/AgentRuntimeConfigFields.tsx b/packages/trueforge-ui/src/atoms/draft/AgentRuntimeConfigFields.tsx index e50fd8cdc..647345439 100644 --- a/packages/trueforge-ui/src/atoms/draft/AgentRuntimeConfigFields.tsx +++ b/packages/trueforge-ui/src/atoms/draft/AgentRuntimeConfigFields.tsx @@ -1,8 +1,9 @@ 'use client'; -import type { AgentRuntimeConfig } from '../../server/types.js'; +import type { AgentCompactionConfig, AgentRuntimeConfig } from '../../server/types.js'; import { cn } from '../lib/cn.js'; import { auiInputClass } from '../lib/inputClasses.js'; +import { PopoverSelect } from '../primitives/PopoverSelect.js'; import { Switch } from '../primitives/Switch.js'; import { Tooltip } from '../primitives/Tooltip.js'; @@ -30,6 +31,14 @@ function parsePositiveInteger(raw: string): number | null { } const NO_SANDBOX_PROVIDER_HINT = 'No sandbox provider yet, add one in Settings → Sandbox'; +const DEFAULT_COMPACTION_THRESHOLD_TOKENS = 50_000; + +const COMPACTION_THRESHOLD_MODE_OPTIONS = [ + { value: 'auto', label: 'Auto' }, + { value: 'custom', label: 'Custom' }, +] as const; + +type CompactionThresholdMode = (typeof COMPACTION_THRESHOLD_MODE_OPTIONS)[number]['value']; type RuntimeSwitchField = { label: string; @@ -50,6 +59,16 @@ export function AgentRuntimeConfigFields({ layout = 'compact', onChange, }: AgentRuntimeConfigFieldsProps) { + const withCompaction = (compaction: AgentCompactionConfig): AgentRuntimeConfig => ({ + ...value, + contextManagement: { + ...value.contextManagement, + compaction, + largeToolResponse: value.contextManagement?.largeToolResponse ?? { enabled: true }, + }, + }); + const applyCompaction = (compaction: AgentCompactionConfig) => onChange(withCompaction(compaction)); + const capabilityFields: RuntimeSwitchField[] = [ { label: 'Dynamic sub-agents', @@ -72,6 +91,8 @@ export function AgentRuntimeConfigFields({ ]; const sandboxEnabled = value.sandbox?.enabled ?? false; const compactionEnabled = value.contextManagement?.compaction?.enabled ?? true; + const thresholdMode: CompactionThresholdMode = + value.contextManagement?.compaction?.trigger != null ? 'custom' : 'auto'; const webSearchField: RuntimeSwitchField | null = webSearchAvailable ? { label: 'Web search', @@ -104,16 +125,9 @@ export function AgentRuntimeConfigFields({ }; const compactionField: RuntimeSwitchField = { label: 'Context compaction', - description: 'Summarize older turns as the context window fills.', + description: 'Summarize older turns as context fills.', checked: compactionEnabled, - update: enabled => ({ - ...value, - contextManagement: { - ...value.contextManagement, - compaction: { ...value.contextManagement?.compaction, enabled }, - largeToolResponse: value.contextManagement?.largeToolResponse ?? { enabled: true }, - }, - }), + update: enabled => withCompaction({ ...value.contextManagement?.compaction, enabled }), }; const largeToolResponseField: RuntimeSwitchField = { label: 'Large tool response offloading', @@ -131,7 +145,8 @@ export function AgentRuntimeConfigFields({ }), }; const runtimeFields = [sandboxField, fileDownloadsField, compactionField, largeToolResponseField]; - const compactionThreshold = value.contextManagement?.compaction?.trigger?.value ?? 50_000; + const compactionThreshold = + value.contextManagement?.compaction?.trigger?.value ?? DEFAULT_COMPACTION_THRESHOLD_TOKENS; const switchField = ({ field, @@ -215,43 +230,59 @@ export function AgentRuntimeConfigFields({
{switchField({ field: compactionField, className: rowClassName })} -
{switchField({ field: largeToolResponseField, className: cn('py-4', rowClassName) })} diff --git a/packages/trueforge-ui/test/atoms/draft/AgentConfigEditors.test.tsx b/packages/trueforge-ui/test/atoms/draft/AgentConfigEditors.test.tsx index 85bc53aff..70b7a3926 100644 --- a/packages/trueforge-ui/test/atoms/draft/AgentConfigEditors.test.tsx +++ b/packages/trueforge-ui/test/atoms/draft/AgentConfigEditors.test.tsx @@ -482,8 +482,10 @@ describe('AgentConfigEditors', () => { expect(screen.getByRole('switch', { name: 'File downloads' })).toBeDisabled(); expect(screen.getByRole('switch', { name: 'File downloads' })).toHaveAttribute('aria-checked', 'false'); - expect(screen.getByRole('spinbutton', { name: /Compaction threshold tokens/ })).toBeDisabled(); - expect(screen.getByRole('spinbutton', { name: /Compaction threshold tokens/ })).toHaveValue(42_000); + expect(screen.getByRole('button', { name: 'Compaction threshold mode' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Compaction threshold mode' })).toHaveTextContent('Custom'); + expect(screen.getByRole('spinbutton', { name: 'Compaction threshold tokens' })).toBeDisabled(); + expect(screen.getByRole('spinbutton', { name: 'Compaction threshold tokens' })).toHaveValue(42_000); fireEvent.click(screen.getByRole('switch', { name: 'Context compaction' })); expect(onChange).toHaveBeenCalledWith({ @@ -501,6 +503,93 @@ describe('AgentConfigEditors', () => { }); }); + it('enables a custom compaction threshold with a 50000-token default', () => { + const onChange = vi.fn(); + render( + + + , + ); + + expect(screen.getByRole('button', { name: 'Compaction threshold mode' })).toHaveTextContent('Auto'); + expect(screen.getByText("Automatically trigger compaction at 80% of model's context window")).toBeInTheDocument(); + expect(screen.queryByRole('spinbutton', { name: 'Compaction threshold tokens' })).not.toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: 'Compaction threshold mode' })); + fireEvent.click(screen.getByRole('option', { name: 'Custom' })); + expect(onChange).toHaveBeenCalledWith({ + model: { name: 'openai/gpt' }, + config: { + contextManagement: { + compaction: { + enabled: true, + trigger: { type: 'input_tokens', value: 50_000 }, + }, + largeToolResponse: { enabled: true }, + }, + }, + }); + }); + + it('clears the compaction trigger when the threshold mode is set to Auto', () => { + const spec: AgentSpec = { + model: { name: 'openai/gpt' }, + config: { + contextManagement: { + compaction: { + enabled: true, + trigger: { type: 'input_tokens', value: 42_000 }, + }, + largeToolResponse: { enabled: true }, + }, + }, + }; + const onChange = vi.fn(); + render( + + + , + ); + + expect(screen.getByRole('spinbutton', { name: 'Compaction threshold tokens' })).toHaveValue(42_000); + expect(screen.getByText('Trigger compaction when input reaches 42,000 tokens')).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: 'Compaction threshold mode' })); + fireEvent.click(screen.getByRole('option', { name: 'Auto' })); + expect(onChange).toHaveBeenCalledWith({ + ...spec, + config: { + ...spec.config, + contextManagement: { + compaction: { enabled: true }, + largeToolResponse: { enabled: true }, + }, + }, + }); + }); + it('selects the first mounted MCP on open, otherwise the first connector', async () => { const loadMcpTools = vi.fn(async (connectorId: string) => [ { id: `${connectorId}.tool`, name: `${connectorId}.tool` },