fix: add env var fallback for heartbeat and cron features (#421)
This commit is contained in:
@@ -33,6 +33,8 @@ describe('normalizeAgents', () => {
|
|||||||
'BLUESKY_HANDLE', 'BLUESKY_APP_PASSWORD', 'BLUESKY_SERVICE_URL', 'BLUESKY_APPVIEW_URL',
|
'BLUESKY_HANDLE', 'BLUESKY_APP_PASSWORD', 'BLUESKY_SERVICE_URL', 'BLUESKY_APPVIEW_URL',
|
||||||
'BLUESKY_NOTIFICATIONS_ENABLED', 'BLUESKY_NOTIFICATIONS_INTERVAL_SEC', 'BLUESKY_NOTIFICATIONS_LIMIT',
|
'BLUESKY_NOTIFICATIONS_ENABLED', 'BLUESKY_NOTIFICATIONS_INTERVAL_SEC', 'BLUESKY_NOTIFICATIONS_LIMIT',
|
||||||
'BLUESKY_NOTIFICATIONS_PRIORITY', 'BLUESKY_NOTIFICATIONS_REASONS',
|
'BLUESKY_NOTIFICATIONS_PRIORITY', 'BLUESKY_NOTIFICATIONS_REASONS',
|
||||||
|
'HEARTBEAT_ENABLED', 'HEARTBEAT_INTERVAL_MIN', 'HEARTBEAT_SKIP_RECENT_USER_MIN',
|
||||||
|
'CRON_ENABLED',
|
||||||
];
|
];
|
||||||
const savedEnv: Record<string, string | undefined> = {};
|
const savedEnv: Record<string, string | undefined> = {};
|
||||||
|
|
||||||
@@ -370,6 +372,135 @@ describe('normalizeAgents', () => {
|
|||||||
expect(agents[0].channels.telegram).toBeUndefined();
|
expect(agents[0].channels.telegram).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should pick up heartbeat from env vars when YAML features is empty', () => {
|
||||||
|
process.env.HEARTBEAT_ENABLED = 'true';
|
||||||
|
process.env.HEARTBEAT_INTERVAL_MIN = '15';
|
||||||
|
process.env.HEARTBEAT_SKIP_RECENT_USER_MIN = '5';
|
||||||
|
|
||||||
|
const config: LettaBotConfig = {
|
||||||
|
server: { mode: 'cloud' },
|
||||||
|
agent: { name: 'TestBot', model: 'test' },
|
||||||
|
channels: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const agents = normalizeAgents(config);
|
||||||
|
|
||||||
|
expect(agents[0].features?.heartbeat).toEqual({
|
||||||
|
enabled: true,
|
||||||
|
intervalMin: 15,
|
||||||
|
skipRecentUserMin: 5,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pick up cron from env vars when YAML features is empty', () => {
|
||||||
|
process.env.CRON_ENABLED = 'true';
|
||||||
|
|
||||||
|
const config: LettaBotConfig = {
|
||||||
|
server: { mode: 'cloud' },
|
||||||
|
agent: { name: 'TestBot', model: 'test' },
|
||||||
|
channels: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const agents = normalizeAgents(config);
|
||||||
|
|
||||||
|
expect(agents[0].features?.cron).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge env var heartbeat into existing YAML features', () => {
|
||||||
|
process.env.HEARTBEAT_ENABLED = 'true';
|
||||||
|
process.env.HEARTBEAT_INTERVAL_MIN = '20';
|
||||||
|
|
||||||
|
const config: LettaBotConfig = {
|
||||||
|
server: { mode: 'cloud' },
|
||||||
|
agent: { name: 'TestBot', model: 'test' },
|
||||||
|
channels: {},
|
||||||
|
features: {
|
||||||
|
cron: true,
|
||||||
|
maxToolCalls: 50,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const agents = normalizeAgents(config);
|
||||||
|
|
||||||
|
// Env var heartbeat should merge in
|
||||||
|
expect(agents[0].features?.heartbeat).toEqual({
|
||||||
|
enabled: true,
|
||||||
|
intervalMin: 20,
|
||||||
|
});
|
||||||
|
// Existing YAML features should be preserved
|
||||||
|
expect(agents[0].features?.cron).toBe(true);
|
||||||
|
expect(agents[0].features?.maxToolCalls).toBe(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not override YAML heartbeat with env vars', () => {
|
||||||
|
process.env.HEARTBEAT_ENABLED = 'true';
|
||||||
|
process.env.HEARTBEAT_INTERVAL_MIN = '99';
|
||||||
|
|
||||||
|
const config: LettaBotConfig = {
|
||||||
|
server: { mode: 'cloud' },
|
||||||
|
agent: { name: 'TestBot', model: 'test' },
|
||||||
|
channels: {},
|
||||||
|
features: {
|
||||||
|
heartbeat: {
|
||||||
|
enabled: true,
|
||||||
|
intervalMin: 10,
|
||||||
|
skipRecentUserMin: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const agents = normalizeAgents(config);
|
||||||
|
|
||||||
|
// YAML values should win
|
||||||
|
expect(agents[0].features?.heartbeat?.intervalMin).toBe(10);
|
||||||
|
expect(agents[0].features?.heartbeat?.skipRecentUserMin).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle heartbeat env var with defaults when interval not set', () => {
|
||||||
|
process.env.HEARTBEAT_ENABLED = 'true';
|
||||||
|
|
||||||
|
const config: LettaBotConfig = {
|
||||||
|
server: { mode: 'cloud' },
|
||||||
|
agent: { name: 'TestBot', model: 'test' },
|
||||||
|
channels: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const agents = normalizeAgents(config);
|
||||||
|
|
||||||
|
expect(agents[0].features?.heartbeat).toEqual({ enabled: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not override YAML cron: false with env var', () => {
|
||||||
|
process.env.CRON_ENABLED = 'true';
|
||||||
|
|
||||||
|
const config: LettaBotConfig = {
|
||||||
|
server: { mode: 'cloud' },
|
||||||
|
agent: { name: 'TestBot', model: 'test' },
|
||||||
|
channels: {},
|
||||||
|
features: {
|
||||||
|
cron: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const agents = normalizeAgents(config);
|
||||||
|
|
||||||
|
expect(agents[0].features?.cron).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not enable heartbeat when env var is not true', () => {
|
||||||
|
process.env.HEARTBEAT_ENABLED = 'false';
|
||||||
|
|
||||||
|
const config: LettaBotConfig = {
|
||||||
|
server: { mode: 'cloud' },
|
||||||
|
agent: { name: 'TestBot', model: 'test' },
|
||||||
|
channels: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const agents = normalizeAgents(config);
|
||||||
|
|
||||||
|
expect(agents[0].features?.heartbeat).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('should pick up all channel types from env vars', () => {
|
it('should pick up all channel types from env vars', () => {
|
||||||
process.env.TELEGRAM_BOT_TOKEN = 'tg-token';
|
process.env.TELEGRAM_BOT_TOKEN = 'tg-token';
|
||||||
process.env.SLACK_BOT_TOKEN = 'slack-bot';
|
process.env.SLACK_BOT_TOKEN = 'slack-bot';
|
||||||
|
|||||||
@@ -639,6 +639,33 @@ export function normalizeAgents(config: LettaBotConfig): AgentConfig[] {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Field-level env var fallback for features (heartbeat, cron).
|
||||||
|
// Unlike channels (all-or-nothing), features are independent toggles so we
|
||||||
|
// merge at the field level: env vars fill in fields missing from YAML.
|
||||||
|
const features = { ...config.features } as NonNullable<LettaBotConfig['features']>;
|
||||||
|
|
||||||
|
if (features.cron == null && process.env.CRON_ENABLED === 'true') {
|
||||||
|
features.cron = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!features.heartbeat && process.env.HEARTBEAT_ENABLED === 'true') {
|
||||||
|
const intervalMin = process.env.HEARTBEAT_INTERVAL_MIN
|
||||||
|
? parseInt(process.env.HEARTBEAT_INTERVAL_MIN, 10)
|
||||||
|
: undefined;
|
||||||
|
const skipRecentUserMin = process.env.HEARTBEAT_SKIP_RECENT_USER_MIN
|
||||||
|
? parseInt(process.env.HEARTBEAT_SKIP_RECENT_USER_MIN, 10)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
features.heartbeat = {
|
||||||
|
enabled: true,
|
||||||
|
...(Number.isFinite(intervalMin) ? { intervalMin } : {}),
|
||||||
|
...(Number.isFinite(skipRecentUserMin) ? { skipRecentUserMin } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only pass features if there's actually something set
|
||||||
|
const hasFeatures = Object.keys(features).length > 0;
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
name: agentName,
|
name: agentName,
|
||||||
id,
|
id,
|
||||||
@@ -646,7 +673,7 @@ export function normalizeAgents(config: LettaBotConfig): AgentConfig[] {
|
|||||||
model,
|
model,
|
||||||
channels,
|
channels,
|
||||||
conversations: config.conversations,
|
conversations: config.conversations,
|
||||||
features: config.features,
|
features: hasFeatures ? features : config.features,
|
||||||
polling: config.polling,
|
polling: config.polling,
|
||||||
integrations: config.integrations,
|
integrations: config.integrations,
|
||||||
}];
|
}];
|
||||||
|
|||||||
Reference in New Issue
Block a user