feat(core): Add get_environment tool for runtime date and timezone (no-changelog) (#29930)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Drury 2026-05-07 08:36:09 +01:00 committed by GitHub
parent 9255311491
commit 4b67c31896
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 113 additions and 0 deletions

View File

@ -636,6 +636,20 @@ export class AgentsService {
const { agent, agentId, projectId, credentialProvider, nodeToolsEnabled, integrationType } =
params;
// Inject get_environment unconditionally. It surfaces info the model
// can't know on its own (current date, instance timezone, day of week)
// via a tool call rather than the system prompt — so values that change
// per request don't bust system-prompt prompt caching.
try {
const { createGetEnvironmentTool } = await import('./tools/environment-tool');
agent.tool(createGetEnvironmentTool());
} catch (toolError) {
this.logger.warn('Failed to inject get_environment tool', {
agentId,
error: toolError instanceof Error ? toolError.message : String(toolError),
});
}
// Inject the rich_interaction tool only for platforms that can actually
// render its suspend/resume HITL cards. Two gates:
// - A registered integration in ChatIntegrationRegistry. The in-app

View File

@ -0,0 +1,71 @@
import { GlobalConfig } from '@n8n/config';
import { Container } from '@n8n/di';
import { DateTime, Settings } from 'luxon';
import { createGetEnvironmentTool } from '../environment-tool';
describe('createGetEnvironmentTool', () => {
const FIXED_NOW_MS = DateTime.fromISO('2026-05-05T14:32:11.000Z', { zone: 'utc' }).toMillis();
beforeEach(() => {
Settings.now = () => FIXED_NOW_MS;
Container.set(GlobalConfig, { generic: { timezone: 'America/New_York' } } as GlobalConfig);
});
afterEach(() => {
Settings.now = () => Date.now();
Container.reset();
});
function makeCtx() {
return {
parentTelemetry: undefined,
};
}
it('builds a tool with the correct name', () => {
const tool = createGetEnvironmentTool().build();
expect(tool.name).toBe('get_environment');
});
it('returns now in the instance timezone with offset', async () => {
const tool = createGetEnvironmentTool().build();
const result = (await tool.handler!({}, makeCtx())) as {
now: string;
timezone: string;
dayOfWeek: string;
};
// 14:32:11Z in America/New_York (DST) = 10:32:11-04:00
expect(result.now).toBe('2026-05-05T10:32:11.000-04:00');
expect(result.timezone).toBe('America/New_York');
expect(result.dayOfWeek).toBe('Tuesday');
});
it('reflects a different configured timezone', async () => {
Container.set(GlobalConfig, { generic: { timezone: 'Asia/Tokyo' } } as GlobalConfig);
const tool = createGetEnvironmentTool().build();
const result = (await tool.handler!({}, makeCtx())) as {
now: string;
timezone: string;
dayOfWeek: string;
};
// 14:32:11Z in Asia/Tokyo = 23:32:11+09:00
expect(result.now).toBe('2026-05-05T23:32:11.000+09:00');
expect(result.timezone).toBe('Asia/Tokyo');
expect(result.dayOfWeek).toBe('Tuesday');
});
it('reads timezone at handler time, not creation time', async () => {
const tool = createGetEnvironmentTool().build();
// Mutate config after the tool is built; the handler must pick up the change.
Container.set(GlobalConfig, { generic: { timezone: 'Europe/London' } } as GlobalConfig);
const result = (await tool.handler!({}, makeCtx())) as { timezone: string };
expect(result.timezone).toBe('Europe/London');
});
});

View File

@ -0,0 +1,28 @@
import { Tool } from '@n8n/agents';
import { GlobalConfig } from '@n8n/config';
import { Container } from '@n8n/di';
import { DateTime } from 'luxon';
import { z } from 'zod';
const DESCRIPTION =
'Returns runtime info that the LLM cannot know on its own: ' +
'current ISO date/time, instance timezone (IANA), and day of week. ' +
'Call when reasoning about "today", deadlines, or scheduling.';
export function createGetEnvironmentTool() {
return (
new Tool('get_environment')
.description(DESCRIPTION)
.input(z.object({}))
// eslint-disable-next-line @typescript-eslint/require-await -- Tool.handler() expects an async callback
.handler(async () => {
const timezone = Container.get(GlobalConfig).generic.timezone;
const now = DateTime.now().setZone(timezone);
return {
now: now.toISO(),
timezone,
dayOfWeek: now.weekdayLong,
};
})
);
}