From 3d8501dba21a923564f65300adaf1b48bee9ddcc Mon Sep 17 00:00:00 2001 From: shars <32410570+Shabonasar@users.noreply.github.com> Date: Fri, 25 Sep 2026 20:51:54 +0300 Subject: [PATCH 1/2] fix(agents): keep dynamic tool context after additional instructions The per-request web search context (current date/time with milliseconds) was joined before additional_instructions, which carry mostly static text such as the artifacts prompt. Provider-side prefix caches stopped at the timestamp, so the static text after it was recomputed on every request. Move the dynamic context to the end via buildAdditionalInstructions and cover the ordering with unit tests. --- packages/api/src/agents/run.spec.ts | 26 ++++++++++++++++++++++++++ packages/api/src/agents/run.ts | 19 ++++++++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/packages/api/src/agents/run.spec.ts b/packages/api/src/agents/run.spec.ts index 9cb7644b6a2..17c1842a35d 100644 --- a/packages/api/src/agents/run.spec.ts +++ b/packages/api/src/agents/run.spec.ts @@ -9,6 +9,7 @@ import { shouldReplayReasoningContent, anyAgentReplaysReasoningContent, collectRunMCPToolAliases, + buildAdditionalInstructions, } from './run'; describe('getRunDiscoveredTools', () => { @@ -471,3 +472,28 @@ describe('collectRunMCPToolAliases', () => { expect(collectRunMCPToolAliases([root] as never)).toEqual([graphAlias]); }); }); + +describe('buildAdditionalInstructions', () => { + it('places dynamic tool context after static additional instructions', () => { + const result = buildAdditionalInstructions( + 'Artifacts prompt', + 'Conversation Date & Time: 2025-01-01T00:00:00.000Z', + ); + + expect(result).toBe('Artifacts prompt\nConversation Date & Time: 2025-01-01T00:00:00.000Z'); + }); + + it('keeps the static prefix identical when only the dynamic context changes', () => { + const first = buildAdditionalInstructions('Artifacts prompt', 'Time: 1'); + const second = buildAdditionalInstructions('Artifacts prompt', 'Time: 2'); + + expect(first.startsWith('Artifacts prompt\n')).toBe(true); + expect(second.startsWith('Artifacts prompt\n')).toBe(true); + }); + + it('handles missing additional instructions and empty dynamic context', () => { + expect(buildAdditionalInstructions(undefined, 'Time: 1')).toBe('Time: 1'); + expect(buildAdditionalInstructions('Artifacts prompt', '')).toBe('Artifacts prompt'); + expect(buildAdditionalInstructions(undefined, '')).toBe(''); + }); +}); diff --git a/packages/api/src/agents/run.ts b/packages/api/src/agents/run.ts index 44f98bc562b..aef0e8ab977 100644 --- a/packages/api/src/agents/run.ts +++ b/packages/api/src/agents/run.ts @@ -223,6 +223,18 @@ export interface RunDiscoverySnapshot { getRunMessages?: () => BaseMessage[] | undefined; } +/** + * Joins additional instructions with per-request tool context. The dynamic part + * (e.g. the current date/time) goes last so that provider-side prefix caches + * can reuse the static part, such as the artifacts prompt, across requests. + */ +export function buildAdditionalInstructions( + additionalInstructions: string | null | undefined, + dynamicToolInstructions: string, +): string { + return [additionalInstructions ?? '', dynamicToolInstructions].join('\n').trim(); +} + /** Reads canonical run discovery state, with best-effort history parsing for older releases. */ export function getRunDiscoveredTools(run: RunDiscoverySnapshot): string[] { if (typeof run.getDiscoveredTools === 'function') { @@ -2412,9 +2424,10 @@ export async function createRun({ const systemContent = [toolInstructions, agent.instructions ?? ''].join('\n').trim(); - const additionalInstructions = [dynamicToolInstructions, agent.additional_instructions ?? ''] - .join('\n') - .trim(); + const additionalInstructions = buildAdditionalInstructions( + agent.additional_instructions, + dynamicToolInstructions, + ); /** Resolves issues with new OpenAI usage field */ if ( From 0c0ecd2344d571200d3af4608e8a7200f944cda6 Mon Sep 17 00:00:00 2001 From: Lia Date: Sun, 27 Sep 2026 12:14:45 +0000 Subject: [PATCH 2/2] fix(agents): keep per-turn instructions after cacheable prompts --- .../src/agents/__tests__/initialize.test.ts | 50 ++++++++++++++++++- .../__tests__/run-summarization.test.ts | 28 ++++++++++- packages/api/src/agents/initialize.ts | 7 ++- packages/api/src/agents/run.ts | 21 ++++---- 4 files changed, 91 insertions(+), 15 deletions(-) diff --git a/packages/api/src/agents/__tests__/initialize.test.ts b/packages/api/src/agents/__tests__/initialize.test.ts index 43bac3fd1db..9a938844163 100644 --- a/packages/api/src/agents/__tests__/initialize.test.ts +++ b/packages/api/src/agents/__tests__/initialize.test.ts @@ -1358,7 +1358,8 @@ describe('initializeAgent — stable and dynamic instruction fields', () => { ); expect(result.instructions).toBeUndefined(); - expect(result.additional_instructions).toBe( + expect(result.additional_instructions).toBeUndefined(); + expect(result.dynamicToolContextMap?.agent_temporal_instructions).toBe( 'Today is 2026-08-31 (Monday). The turn began at 2026-08-31 06:20:00 +00:00 (Monday) (2026-08-31T06:20:00.000Z).', ); }); @@ -1383,11 +1384,56 @@ describe('initializeAgent — stable and dynamic instruction fields', () => { ); expect(result.instructions).toBeUndefined(); - expect(result.additional_instructions).toBe( + expect(result.additional_instructions).toBeUndefined(); + expect(result.dynamicToolContextMap?.agent_temporal_instructions).toBe( 'It is currently 2024-01-15 13:30:00 -05:00 (Monday).', ); }); + it('keeps artifact guidance before all per-turn context without mutating shared tool context', async () => { + const { generateArtifactsPrompt } = jest.requireMock('~/prompts') as { + generateArtifactsPrompt: jest.Mock; + }; + generateArtifactsPrompt.mockReturnValue('Artifact guidance'); + + const { agent, req, res, loadTools, db } = createMocks(); + const webContext = { web_search: 'Conversation Date & Time: 2026-08-31T06:20:00.000Z' }; + loadTools.mockResolvedValueOnce({ + tools: [], + toolContextMap: {}, + dynamicToolContextMap: webContext, + toolDefinitions: [], + hasDeferredTools: false, + }); + agent.additional_instructions = 'Stable agent context'; + agent.instructions = 'Today is {{current_date}}.'; + agent.artifacts = 'enabled' as never; + req.turnStartedAt = new Date('2026-08-31T06:20:00.000Z').getTime(); + req.body = { timezone: 'UTC' }; + + const result = await initializeAgent( + { + req, + res, + agent, + loadTools, + endpointOption: { endpoint: EModelEndpoint.agents }, + allowedProviders: new Set([Providers.OPENAI]), + isInitialAgent: true, + }, + db, + ); + + expect(result.additional_instructions).toBe('Stable agent context\n\nArtifact guidance'); + expect(result.dynamicToolContextMap).toEqual({ + ...webContext, + agent_temporal_instructions: 'Today is 2026-08-31 (Monday).', + }); + expect(webContext).toEqual({ + web_search: 'Conversation Date & Time: 2026-08-31T06:20:00.000Z', + }); + }); + it('keeps non-temporal special vars in stable instructions', async () => { const { agent, req, res, loadTools, db } = createMocks(); agent.instructions = 'You are helping {{current_user}}.'; diff --git a/packages/api/src/agents/__tests__/run-summarization.test.ts b/packages/api/src/agents/__tests__/run-summarization.test.ts index 3b7e781e1c9..53a6175d819 100644 --- a/packages/api/src/agents/__tests__/run-summarization.test.ts +++ b/packages/api/src/agents/__tests__/run-summarization.test.ts @@ -2086,7 +2086,29 @@ describe('stable/dynamic system instructions', () => { }); expect(agents[0].instructions).toBe('Static tool instructions\nBase instructions'); - expect(agents[0].additional_instructions).toBe('Conversation Date & Time: anchor\nMemory tail'); + expect(agents[0].additional_instructions).toBe('Memory tail\nConversation Date & Time: anchor'); + }); + + it('preserves the full artifact prefix when timestamps change between turns', async () => { + const agents = await callAndCapture({ + agents: ['2026-08-31T06:20:00.000Z', '2026-08-31T06:21:00.000Z'].map((time, index) => + makeAgent({ + id: `agent_${index}`, + instructions: 'Stable agent instructions', + additional_instructions: 'Artifact guidance', + toolContextMap: { web_search: 'Static web instructions' }, + dynamicToolContextMap: { web_search: `Conversation Date & Time: ${time}` }, + }), + ), + }); + + expect(agents[0].instructions).toBe(agents[1].instructions); + expect(agents[0].additional_instructions).toBe( + 'Artifact guidance\nConversation Date & Time: 2026-08-31T06:20:00.000Z', + ); + expect(agents[1].additional_instructions).toBe( + 'Artifact guidance\nConversation Date & Time: 2026-08-31T06:21:00.000Z', + ); }); }); @@ -3192,6 +3214,7 @@ describe('subagentConfigs', () => { const member = makeAgent({ id: 'agent_skilled_member', additional_instructions: 'Keep the response concise.', + dynamicToolContextMap: { web_search: 'Conversation Date & Time: anchor' }, alwaysApplySkillPrimes: [ { name: 'member-workflow', body: 'Follow the member-specific workflow.' }, ], @@ -3218,7 +3241,8 @@ describe('subagentConfigs', () => { const [memberInput] = config.agents as Array>; expect(memberInput.additional_instructions).toBe( 'Keep the response concise.\n\n' + - '# Always-apply skill: member-workflow\nFollow the member-specific workflow.', + '# Always-apply skill: member-workflow\nFollow the member-specific workflow.\n' + + 'Conversation Date & Time: anchor', ); }); diff --git a/packages/api/src/agents/initialize.ts b/packages/api/src/agents/initialize.ts index 9a09ac949c2..f58e0e808be 100644 --- a/packages/api/src/agents/initialize.ts +++ b/packages/api/src/agents/initialize.ts @@ -2251,6 +2251,7 @@ export async function initializeAgent( (agent.model_parameters as Record).configuration = options.configOptions; } + let temporalInstructions: string | undefined; if (agent.instructions && agent.instructions !== '') { const resolvedInstructions = replaceSpecialVars({ text: agent.instructions, @@ -2260,7 +2261,7 @@ export async function initializeAgent( }); if (hasTemporalSpecialVars(agent.instructions)) { agent.instructions = undefined; - appendAdditionalInstructions(agent, resolvedInstructions); + temporalInstructions = resolvedInstructions; } else { agent.instructions = resolvedInstructions; } @@ -2524,7 +2525,9 @@ export async function initializeAgent( agentContextAttachments, fileConsumers, toolContextMap: toolContextMap ?? {}, - dynamicToolContextMap: dynamicToolContextMap ?? {}, + dynamicToolContextMap: temporalInstructions + ? { ...dynamicToolContextMap, agent_temporal_instructions: temporalInstructions } + : (dynamicToolContextMap ?? {}), useLegacyContent: !!options.useLegacyContent, tools: (tools ?? []) as GenericTool[] & string[], maxToolResultChars: maxToolResultCharsResolved, diff --git a/packages/api/src/agents/run.ts b/packages/api/src/agents/run.ts index aef0e8ab977..8703c9ba779 100644 --- a/packages/api/src/agents/run.ts +++ b/packages/api/src/agents/run.ts @@ -1842,16 +1842,19 @@ function buildIsolatedAgentInputs( child: RunAgent, toInput: (agent: RunAgent, opts?: { isSubagent?: boolean }) => AgentInputs, ): AgentInputs { - const childInputs = toInput(child, { isSubagent: true }); const alwaysApplySkillPrimes = child.alwaysApplySkillPrimes; - if (alwaysApplySkillPrimes && alwaysApplySkillPrimes.length > 0) { - const skillInstructions = alwaysApplySkillPrimes - .map((prime) => `# Always-apply skill: ${prime.name}\n${prime.body}`) - .join('\n\n'); - childInputs.additional_instructions = [childInputs.additional_instructions, skillInstructions] - .filter((value): value is string => typeof value === 'string' && value.length > 0) - .join('\n\n'); - } + const skillInstructions = alwaysApplySkillPrimes + ?.map((prime) => `# Always-apply skill: ${prime.name}\n${prime.body}`) + .join('\n\n'); + const childWithSkill = skillInstructions + ? { + ...child, + additional_instructions: [child.additional_instructions, skillInstructions] + .filter((value): value is string => typeof value === 'string' && value.length > 0) + .join('\n\n'), + } + : child; + const childInputs = toInput(childWithSkill, { isSubagent: true }); if ((child.backgroundToolNames?.length ?? 0) > 0) { childInputs.toolDefinitions = stripBackgroundFromToolDefinitions( childInputs.toolDefinitions,