Skip to content
Merged
4 changes: 2 additions & 2 deletions packages/api/src/agents/__tests__/run-summarization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ describe('summarizationConfig field passthrough', () => {
const agents = await callAndCapture({
summarizationConfig: {
enabled: true,
trigger: { type: 'token_count', value: 8000 },
trigger: { type: 'token_ratio', value: 0.8 },
provider: 'anthropic',
model: 'claude-3-haiku',
parameters: { temperature: 0.2 },
Expand All @@ -233,7 +233,7 @@ describe('summarizationConfig field passthrough', () => {
// `enabled` is not forwarded to the agent-level config β€” it is resolved
// into the separate `summarizationEnabled` boolean on the agent input.
expect(agents[0].summarizationEnabled).toBe(true);
expect(config.trigger).toEqual({ type: 'token_count', value: 8000 });
expect(config.trigger).toEqual({ type: 'token_ratio', value: 0.8 });
expect(config.provider).toBe('anthropic');
expect(config.model).toBe('claude-3-haiku');
expect(config.parameters).toEqual({ temperature: 0.2 });
Expand Down
85 changes: 85 additions & 0 deletions packages/data-provider/specs/config-schemas.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
interfaceSchema,
fileStorageSchema,
fileStrategiesSchema,
summarizationTriggerSchema,
summarizationConfigSchema,
} from '../src/config';
import { tModelSpecPresetSchema, EModelEndpoint } from '../src/schemas';
import { FileSources } from '../src/types/files';
Expand Down Expand Up @@ -502,3 +504,86 @@ describe('interfaceSchema', () => {
expect(result.modelSelect).toBe(false);
});
});

describe('summarizationTriggerSchema', () => {
it.each([
['token_ratio', 0.8],
['remaining_tokens', 500],
['messages_to_refine', 4],
] as const)('accepts documented trigger type "%s" with a sensible value', (type, value) => {
const result = summarizationTriggerSchema.safeParse({ type, value });
expect(result.success).toBe(true);
});

it('rejects the legacy/typoed "token_count" trigger type', () => {
const result = summarizationTriggerSchema.safeParse({
type: 'token_count',
value: 8000,
});
expect(result.success).toBe(false);
});

it('rejects unknown trigger types', () => {
const result = summarizationTriggerSchema.safeParse({
type: 'never_heard_of_it',
value: 1,
});
expect(result.success).toBe(false);
});

it('rejects negative values on any trigger type', () => {
expect(summarizationTriggerSchema.safeParse({ type: 'token_ratio', value: -0.5 }).success).toBe(
false,
);
expect(
summarizationTriggerSchema.safeParse({ type: 'remaining_tokens', value: -1 }).success,
).toBe(false);
expect(
summarizationTriggerSchema.safeParse({ type: 'messages_to_refine', value: -1 }).success,
).toBe(false);
});

it('rejects zero for count-based triggers where it has no meaningful effect', () => {
expect(
summarizationTriggerSchema.safeParse({ type: 'remaining_tokens', value: 0 }).success,
).toBe(false);
expect(
summarizationTriggerSchema.safeParse({ type: 'messages_to_refine', value: 0 }).success,
).toBe(false);
});

it('rejects token_ratio values > 1 to catch the "80 meant as 80%" mistake', () => {
expect(summarizationTriggerSchema.safeParse({ type: 'token_ratio', value: 80 }).success).toBe(
false,
);
expect(summarizationTriggerSchema.safeParse({ type: 'token_ratio', value: 1.01 }).success).toBe(
false,
);
});

it('accepts token_ratio values at the inclusive 0 and 1 bounds per docs', () => {
expect(summarizationTriggerSchema.safeParse({ type: 'token_ratio', value: 0 }).success).toBe(
true,
);
expect(summarizationTriggerSchema.safeParse({ type: 'token_ratio', value: 1 }).success).toBe(
true,
);
});

it('allows remaining_tokens and messages_to_refine values above 1 (token/message counts)', () => {
expect(
summarizationTriggerSchema.safeParse({ type: 'remaining_tokens', value: 2000 }).success,
).toBe(true);
expect(
summarizationTriggerSchema.safeParse({ type: 'messages_to_refine', value: 20 }).success,
).toBe(true);
});

it('parses inside the full summarization config', () => {
const result = summarizationConfigSchema.safeParse({
enabled: true,
trigger: { type: 'token_ratio', value: 0.8 },
});
expect(result.success).toBe(true);
});
});
18 changes: 14 additions & 4 deletions packages/data-provider/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1020,10 +1020,20 @@ export const memorySchema = z.object({

export type TMemoryConfig = DeepPartial<z.infer<typeof memorySchema>>;

export const summarizationTriggerSchema = z.object({
type: z.enum(['token_count']),
value: z.number().positive(),
});
export const summarizationTriggerSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('token_ratio'),
value: z.number().min(0).max(1),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve zero-valued token_ratio triggers

token_ratio now accepts value: 0 here, but shapeSummarizationConfig forwards trigger only when config?.trigger?.value is truthy (packages/api/src/agents/run.ts, around lines 197-200). With value: 0, the trigger is silently dropped and runtime falls back to β€œno trigger configured,” which changes behavior (e.g., it can summarize on pruning even when token-ratio inputs are unavailable) instead of honoring the explicit ratio trigger. This makes a documented valid config behave differently than configured.

Useful? React with πŸ‘Β / πŸ‘Ž.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch β€” addressed in 1aba326. shapeSummarizationConfig was using a truthy check on config?.trigger?.value which dropped value: 0 silently. Switched to typeof value === 'number' so the trigger survives and the runtime honors the explicit ratio instead of falling back to the no-trigger default. Added a regression test for the { type: 'token_ratio', value: 0 } passthrough.

}),
z.object({
type: z.literal('remaining_tokens'),
value: z.number().positive(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Disallow Infinity in count-based trigger values

The new remaining_tokens/messages_to_refine branches validate value with z.number().positive(), which in Zod still accepts Infinity (e.g., YAML .inf). That means config validation can succeed, but the summarization runtime rejects non-finite trigger values and returns false, so summarization silently never fires even though the server starts cleanly. This mismatch is user-visible whenever trigger.value becomes infinite and should be blocked at schema level (e.g., .finite()) to keep validation and runtime behavior aligned.

Useful? React with πŸ‘Β / πŸ‘Ž.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in a2731a9 β€” added .finite() to every trigger value. z.number().positive() accepts Infinity (and z.number() accepts NaN), which would pass validation and then silently no-op because the agents SDK guards every path with Number.isFinite(...). .finite() is applied uniformly (even to token_ratio where .max(1) already rules out Infinity but not NaN).

}),
z.object({
type: z.literal('messages_to_refine'),
value: z.number().positive(),
}),
]);

export const contextPruningSchema = z.object({
enabled: z.boolean().optional(),
Expand Down
Loading