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
76 changes: 76 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,77 @@ 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 zero or negative values', () => {
expect(summarizationTriggerSchema.safeParse({ type: 'token_ratio', value: 0 }).success).toBe(
false,
);
expect(summarizationTriggerSchema.safeParse({ type: 'token_ratio', value: -0.5 }).success).toBe(
false,
);
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 value at the upper bound of 1', () => {
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().positive().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 Allow documented token_ratio: 0 trigger threshold

The new token_ratio branch rejects value: 0 because it uses .positive().max(1), but the published trigger range is 0.0–1.0; this means a documented configuration now fails schema validation and can stop startup when configSchema.strict().safeParse(...) runs in config loading. This regression only appears when admins intentionally set trigger.type: "token_ratio" with value: 0 (e.g., to force immediate summarization), and the fix is to include 0 in the accepted lower bound for that variant.

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 bc4e2c8 β€” switched token_ratio from .positive().max(1) to .min(0).max(1) to match the documented inclusive 0.0–1.0 range. 0 is a valid extreme that means "summarize on every pruning event" per the SDK's usedRatio >= 0 check. remaining_tokens and messages_to_refine keep .positive() β€” 0 on either is semantically no-op given the SDK's messagesToRefineCount <= 0 early return.

}),
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