Skip to content

Commit 32a123f

Browse files
authored
feat(core): inject memory and JIT context into subagents (#23032)
1 parent 23264ce commit 32a123f

2 files changed

Lines changed: 124 additions & 6 deletions

File tree

packages/core/src/agents/local-executor.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3373,5 +3373,104 @@ describe('LocalAgentExecutor', () => {
33733373
const uniqueNames = new Set(names);
33743374
expect(uniqueNames.size).toBe(names.length);
33753375
});
3376+
3377+
describe('Memory Injection', () => {
3378+
it('should inject system instruction memory into system prompt', async () => {
3379+
const definition = createTestDefinition();
3380+
const executor = await LocalAgentExecutor.create(
3381+
definition,
3382+
mockConfig,
3383+
onActivity,
3384+
);
3385+
3386+
const mockMemory = 'Global memory constraint';
3387+
vi.spyOn(mockConfig, 'getSystemInstructionMemory').mockReturnValue(
3388+
mockMemory,
3389+
);
3390+
3391+
mockModelResponse([
3392+
{
3393+
name: TASK_COMPLETE_TOOL_NAME,
3394+
args: { finalResult: 'done' },
3395+
id: 'call1',
3396+
},
3397+
]);
3398+
3399+
await executor.run({ goal: 'test' }, signal);
3400+
3401+
const chatConstructorArgs = MockedGeminiChat.mock.calls[0];
3402+
const systemInstruction = chatConstructorArgs[1] as string;
3403+
3404+
expect(systemInstruction).toContain(mockMemory);
3405+
expect(systemInstruction).toContain('<loaded_context>');
3406+
});
3407+
3408+
it('should inject environment memory into the first message when JIT is disabled', async () => {
3409+
const definition = createTestDefinition();
3410+
const executor = await LocalAgentExecutor.create(
3411+
definition,
3412+
mockConfig,
3413+
onActivity,
3414+
);
3415+
3416+
const mockMemory = 'Project memory rule';
3417+
vi.spyOn(mockConfig, 'getEnvironmentMemory').mockReturnValue(
3418+
mockMemory,
3419+
);
3420+
vi.spyOn(mockConfig, 'isJitContextEnabled').mockReturnValue(false);
3421+
3422+
mockModelResponse([
3423+
{
3424+
name: TASK_COMPLETE_TOOL_NAME,
3425+
args: { finalResult: 'done' },
3426+
id: 'call1',
3427+
},
3428+
]);
3429+
3430+
await executor.run({ goal: 'test' }, signal);
3431+
3432+
const { message } = getMockMessageParams(0);
3433+
const parts = message as Part[];
3434+
3435+
expect(parts).toBeDefined();
3436+
const memoryPart = parts.find((p) => p.text?.includes(mockMemory));
3437+
expect(memoryPart).toBeDefined();
3438+
expect(memoryPart?.text).toBe(mockMemory);
3439+
});
3440+
3441+
it('should inject session memory into the first message when JIT is enabled', async () => {
3442+
const definition = createTestDefinition();
3443+
const executor = await LocalAgentExecutor.create(
3444+
definition,
3445+
mockConfig,
3446+
onActivity,
3447+
);
3448+
3449+
const mockMemory =
3450+
'<loaded_context>\nExtension memory rule\n</loaded_context>';
3451+
vi.spyOn(mockConfig, 'getSessionMemory').mockReturnValue(mockMemory);
3452+
vi.spyOn(mockConfig, 'isJitContextEnabled').mockReturnValue(true);
3453+
3454+
mockModelResponse([
3455+
{
3456+
name: TASK_COMPLETE_TOOL_NAME,
3457+
args: { finalResult: 'done' },
3458+
id: 'call1',
3459+
},
3460+
]);
3461+
3462+
await executor.run({ goal: 'test' }, signal);
3463+
3464+
const { message } = getMockMessageParams(0);
3465+
const parts = message as Part[];
3466+
3467+
expect(parts).toBeDefined();
3468+
const memoryPart = parts.find((p) =>
3469+
p.text?.includes('Extension memory rule'),
3470+
);
3471+
expect(memoryPart).toBeDefined();
3472+
expect(memoryPart?.text).toContain(mockMemory);
3473+
});
3474+
});
33763475
});
33773476
});

packages/core/src/agents/local-executor.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { CompressionStatus } from '../core/turn.js';
3232
import { type ToolCallRequestInfo } from '../scheduler/types.js';
3333
import { ChatCompressionService } from '../services/chatCompressionService.js';
3434
import { getDirectoryContextString } from '../utils/environmentContext.js';
35+
import { renderUserMemory } from '../prompts/snippets.js';
3536
import { promptIdContext } from '../utils/promptIdContext.js';
3637
import {
3738
logAgentStart,
@@ -585,12 +586,24 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
585586
);
586587
const formattedInitialHints = formatUserHintsForModel(initialHints);
587588

588-
let currentMessage: Content = formattedInitialHints
589-
? {
590-
role: 'user',
591-
parts: [{ text: formattedInitialHints }, { text: query }],
592-
}
593-
: { role: 'user', parts: [{ text: query }] };
589+
// Inject loaded memory files (JIT + extension/project memory)
590+
const environmentMemory = this.context.config.isJitContextEnabled?.()
591+
? this.context.config.getSessionMemory()
592+
: this.context.config.getEnvironmentMemory();
593+
594+
const initialParts: Part[] = [];
595+
if (environmentMemory) {
596+
initialParts.push({ text: environmentMemory });
597+
}
598+
if (formattedInitialHints) {
599+
initialParts.push({ text: formattedInitialHints });
600+
}
601+
initialParts.push({ text: query });
602+
603+
let currentMessage: Content = {
604+
role: 'user',
605+
parts: initialParts,
606+
};
594607

595608
while (true) {
596609
// Check for termination conditions like max turns.
@@ -1375,6 +1388,12 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
13751388
// Inject user inputs into the prompt template.
13761389
let finalPrompt = templateString(promptConfig.systemPrompt, inputs);
13771390

1391+
// Append memory context if available.
1392+
const systemMemory = this.context.config.getSystemInstructionMemory();
1393+
if (systemMemory) {
1394+
finalPrompt += `\n\n${renderUserMemory(systemMemory)}`;
1395+
}
1396+
13781397
// Append environment context (CWD and folder structure).
13791398
const dirContext = await getDirectoryContextString(this.context.config);
13801399
finalPrompt += `\n\n# Environment Context\n${dirContext}`;

0 commit comments

Comments
 (0)