mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-20 18:14:29 -07:00
feat(sdk): Implement dynamic system instructions (#18863)
Co-authored-by: N. Taylor Mullen <ntaylormullen@google.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { GeminiCliAgent } from './agent.js';
|
||||
import * as path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Set this to true locally when you need to update snapshots
|
||||
const RECORD_MODE = process.env['RECORD_NEW_RESPONSES'] === 'true';
|
||||
|
||||
const getGoldenPath = (name: string) =>
|
||||
path.resolve(__dirname, '../test-data', `${name}.json`);
|
||||
|
||||
describe('GeminiCliAgent Integration', () => {
|
||||
it('handles static instructions', async () => {
|
||||
const goldenFile = getGoldenPath('agent-static-instructions');
|
||||
|
||||
const agent = new GeminiCliAgent({
|
||||
instructions: 'You are a pirate. Respond in pirate speak.',
|
||||
model: 'gemini-2.0-flash',
|
||||
recordResponses: RECORD_MODE ? goldenFile : undefined,
|
||||
fakeResponses: RECORD_MODE ? undefined : goldenFile,
|
||||
});
|
||||
|
||||
const events = [];
|
||||
const stream = agent.sendStream('Say hello.');
|
||||
|
||||
for await (const event of stream) {
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
const textEvents = events.filter((e) => e.type === 'content');
|
||||
const responseText = textEvents
|
||||
.map((e) => (typeof e.value === 'string' ? e.value : ''))
|
||||
.join('');
|
||||
|
||||
// Expect pirate speak
|
||||
expect(responseText.toLowerCase()).toMatch(/ahoy|matey|arrr/);
|
||||
}, 30000);
|
||||
|
||||
it('handles dynamic instructions', async () => {
|
||||
const goldenFile = getGoldenPath('agent-dynamic-instructions');
|
||||
|
||||
let callCount = 0;
|
||||
const agent = new GeminiCliAgent({
|
||||
instructions: (_ctx) => {
|
||||
callCount++;
|
||||
return `You are a helpful assistant. The secret number is ${callCount}. Always mention the secret number when asked.`;
|
||||
},
|
||||
model: 'gemini-2.0-flash',
|
||||
recordResponses: RECORD_MODE ? goldenFile : undefined,
|
||||
fakeResponses: RECORD_MODE ? undefined : goldenFile,
|
||||
});
|
||||
|
||||
// First turn
|
||||
const stream1 = agent.sendStream('What is the secret number?');
|
||||
const events1 = [];
|
||||
for await (const event of stream1) {
|
||||
events1.push(event);
|
||||
}
|
||||
const responseText1 = events1
|
||||
.filter((e) => e.type === 'content')
|
||||
.map((e) => (typeof e.value === 'string' ? e.value : ''))
|
||||
.join('');
|
||||
|
||||
expect(responseText1).toContain('1');
|
||||
expect(callCount).toBe(1);
|
||||
|
||||
// Second turn
|
||||
const stream2 = agent.sendStream('What is the secret number now?');
|
||||
const events2 = [];
|
||||
for await (const event of stream2) {
|
||||
events2.push(event);
|
||||
}
|
||||
const responseText2 = events2
|
||||
.filter((e) => e.type === 'content')
|
||||
.map((e) => (typeof e.value === 'string' ? e.value : ''))
|
||||
.join('');
|
||||
|
||||
// Should still be 1 because instructions are only loaded once per session
|
||||
expect(responseText2).toContain('1');
|
||||
expect(callCount).toBe(1);
|
||||
}, 30000);
|
||||
|
||||
it('handles async dynamic instructions', async () => {
|
||||
const goldenFile = getGoldenPath('agent-async-instructions');
|
||||
|
||||
let callCount = 0;
|
||||
const agent = new GeminiCliAgent({
|
||||
instructions: async (_ctx) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10)); // Simulate async work
|
||||
callCount++;
|
||||
return `You are a helpful assistant. The secret number is ${callCount}. Always mention the secret number when asked.`;
|
||||
},
|
||||
model: 'gemini-2.0-flash',
|
||||
recordResponses: RECORD_MODE ? goldenFile : undefined,
|
||||
fakeResponses: RECORD_MODE ? undefined : goldenFile,
|
||||
});
|
||||
|
||||
// First turn
|
||||
const stream1 = agent.sendStream('What is the secret number?');
|
||||
const events1 = [];
|
||||
for await (const event of stream1) {
|
||||
events1.push(event);
|
||||
}
|
||||
const responseText1 = events1
|
||||
.filter((e) => e.type === 'content')
|
||||
.map((e) => (typeof e.value === 'string' ? e.value : ''))
|
||||
.join('');
|
||||
|
||||
expect(responseText1).toContain('1');
|
||||
expect(callCount).toBe(1);
|
||||
|
||||
// Second turn
|
||||
const stream2 = agent.sendStream('What is the secret number now?');
|
||||
const events2 = [];
|
||||
for await (const event of stream2) {
|
||||
events2.push(event);
|
||||
}
|
||||
const responseText2 = events2
|
||||
.filter((e) => e.type === 'content')
|
||||
.map((e) => (typeof e.value === 'string' ? e.value : ''))
|
||||
.join('');
|
||||
|
||||
// Should still be 1 because instructions are only loaded once per session
|
||||
expect(responseText2).toContain('1');
|
||||
expect(callCount).toBe(1);
|
||||
}, 30000);
|
||||
|
||||
it('throws when dynamic instructions fail', async () => {
|
||||
const agent = new GeminiCliAgent({
|
||||
instructions: () => {
|
||||
throw new Error('Dynamic instruction failure');
|
||||
},
|
||||
model: 'gemini-2.0-flash',
|
||||
});
|
||||
|
||||
const stream = agent.sendStream('Say hello.');
|
||||
|
||||
await expect(async () => {
|
||||
for await (const _event of stream) {
|
||||
// Just consume the stream
|
||||
}
|
||||
}).rejects.toThrow('Dynamic instruction failure');
|
||||
});
|
||||
});
|
||||
@@ -24,8 +24,12 @@ import { SdkAgentFilesystem } from './fs.js';
|
||||
import { SdkAgentShell } from './shell.js';
|
||||
import type { SessionContext } from './types.js';
|
||||
|
||||
export type SystemInstructions =
|
||||
| string
|
||||
| ((context: SessionContext) => string | Promise<string>);
|
||||
|
||||
export interface GeminiCliAgentOptions {
|
||||
instructions: string;
|
||||
instructions: SystemInstructions;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
tools?: Array<Tool<any>>;
|
||||
model?: string;
|
||||
@@ -39,18 +43,24 @@ export class GeminiCliAgent {
|
||||
private config: Config;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private tools: Array<Tool<any>>;
|
||||
private readonly instructions: SystemInstructions;
|
||||
private instructionsLoaded = false;
|
||||
|
||||
constructor(options: GeminiCliAgentOptions) {
|
||||
this.instructions = options.instructions;
|
||||
const cwd = options.cwd || process.cwd();
|
||||
this.tools = options.tools || [];
|
||||
|
||||
const initialMemory =
|
||||
typeof this.instructions === 'string' ? this.instructions : '';
|
||||
|
||||
const configParams: ConfigParameters = {
|
||||
sessionId: `sdk-${Date.now()}`,
|
||||
targetDir: cwd,
|
||||
cwd,
|
||||
debugMode: options.debug ?? false,
|
||||
model: options.model || PREVIEW_GEMINI_MODEL_AUTO,
|
||||
userMemory: options.instructions,
|
||||
userMemory: initialMemory,
|
||||
// Minimal config
|
||||
enableHooks: false,
|
||||
mcpEnabled: false,
|
||||
@@ -94,6 +104,30 @@ export class GeminiCliAgent {
|
||||
{ text: prompt },
|
||||
];
|
||||
|
||||
if (!this.instructionsLoaded && typeof this.instructions === 'function') {
|
||||
const context: SessionContext = {
|
||||
sessionId,
|
||||
transcript: client.getHistory(),
|
||||
cwd: this.config.getWorkingDir(),
|
||||
timestamp: new Date().toISOString(),
|
||||
fs,
|
||||
shell,
|
||||
agent: this,
|
||||
};
|
||||
try {
|
||||
const newInstructions = await this.instructions(context);
|
||||
this.config.setUserMemory(newInstructions);
|
||||
client.updateSystemInstruction();
|
||||
this.instructionsLoaded = true;
|
||||
} catch (e) {
|
||||
const error =
|
||||
e instanceof Error
|
||||
? e
|
||||
: new Error(`Error resolving dynamic instructions: ${String(e)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
// sendMessageStream returns AsyncGenerator<ServerGeminiStreamEvent, Turn>
|
||||
const stream = client.sendMessageStream(request, abortSignal, sessionId);
|
||||
|
||||
Reference in New Issue
Block a user