Prototype self-reflection.

This commit is contained in:
Christian Gunderman
2026-03-09 15:20:48 -07:00
parent 680077631d
commit 6b47b0a8f8
19 changed files with 291 additions and 1 deletions
+2 -1
View File
@@ -2,7 +2,8 @@
"experimental": {
"plan": true,
"extensionReloading": true,
"modelSteering": true
"modelSteering": true,
"reflection": true
},
"general": {
"devtools": true
+1
View File
@@ -776,6 +776,7 @@ export async function loadCliConfig(
skillsSupport: settings.skills?.enabled ?? true,
disabledSkills: settings.skills?.disabled,
experimentalJitContext: settings.experimental?.jitContext,
experimentalReflection: settings.experimental?.reflection,
modelSteering: settings.experimental?.modelSteering,
toolOutputMasking: settings.experimental?.toolOutputMasking,
noBrowser: !!process.env['NO_BROWSER'],
+10
View File
@@ -1798,6 +1798,16 @@ const SETTINGS_SCHEMA = {
description: 'Enable Just-In-Time (JIT) context loading.',
showInDialog: false,
},
reflection: {
type: 'boolean',
label: 'Continuous Learning (Reflection)',
category: 'Experimental',
requiresRestart: false,
default: false,
description:
'Enable the agent to periodically reflect on the session and propose new skills or memories.',
showInDialog: true,
},
useOSC52Paste: {
type: 'boolean',
label: 'Use OSC 52 Paste',
+101
View File
@@ -0,0 +1,101 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { LocalAgentDefinition } from './types.js';
import {
GLOB_TOOL_NAME,
GREP_TOOL_NAME,
LS_TOOL_NAME,
READ_FILE_TOOL_NAME,
WRITE_FILE_TOOL_NAME,
EDIT_TOOL_NAME,
GET_SESSION_HISTORY_TOOL_NAME,
} from '../tools/tool-names.js';
import { DEFAULT_GEMINI_MODEL } from '../config/models.js';
import { z } from 'zod';
import type { Config } from '../config/config.js';
// Define a type that matches the outputConfig schema for type safety.
const ReflectAgentReportSchema = z.object({
SummaryOfFindings: z
.string()
.describe(
'A summary of what was learned during reflection, including any new skills or memories created.',
),
CreatedSkills: z
.array(z.string())
.describe('A list of skill files created or updated.'),
AddedMemories: z
.array(z.string())
.describe('A list of global memories added to GEMINI.md.'),
});
/**
* A subagent specialized in reflecting on session history and preserving
* reusable knowledge as skills or memories.
*/
export const ReflectAgent = (
_config: Config,
): LocalAgentDefinition<typeof ReflectAgentReportSchema> => ({
name: 'reflect_agent',
kind: 'local',
displayName: 'Reflect Agent',
description: `A specialized agent that reads the current chat history, identifies reusable knowledge or workflows, and saves them as skills or memories.`,
inputConfig: {
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
outputConfig: {
outputName: 'report',
description: 'The final reflection report as a JSON object.',
schema: ReflectAgentReportSchema,
},
processOutput: (output) => JSON.stringify(output, null, 2),
modelConfig: {
model: DEFAULT_GEMINI_MODEL,
generateContentConfig: {
temperature: 0.1,
topP: 0.95,
},
},
runConfig: {
maxTimeMinutes: 3,
maxTurns: 10,
},
toolConfig: {
tools: [
GET_SESSION_HISTORY_TOOL_NAME,
LS_TOOL_NAME,
READ_FILE_TOOL_NAME,
GLOB_TOOL_NAME,
GREP_TOOL_NAME,
WRITE_FILE_TOOL_NAME,
EDIT_TOOL_NAME,
],
},
promptConfig: {
query: `Please review the current session history and save any valuable learnings as skills or memories.`,
systemPrompt: `You are the **Reflect Agent**, a specialized AI agent responsible for continuous learning. Your purpose is to review the current session's chat history, identify high-value, reusable knowledge or workflows, and persist them.
## Core Directives
1. **Retrieve History:** Your very first action MUST be to call the \`get_session_history\` tool to read what happened during this session.
2. **Analyze & Extract:** Look for complex workflows, specific project conventions, repeated commands, or user preferences that the agent should remember for future tasks.
3. **Persist Knowledge:**
- **Memories:** For general, workspace-level preferences (e.g., "always use 2 spaces", "use strict typescript"), update the \`GEMINI.md\` file using \`${WRITE_FILE_TOOL_NAME}\` or \`${EDIT_TOOL_NAME}\`.
- **Skills:** For complex, task-specific scripts or workflows, create a new skill in the \`.gemini/skills/\` directory. A skill requires a \`SKILL.md\` file containing the rules and resources.
4. **Local Changes Only:** Only modify files in the \`.gemini/\` directory or the \`GEMINI.md\` file. Do NOT modify the main application source code.
5. **Format Report:** Once you have saved the learnings, call the \`complete_task\` tool with a structured JSON report detailing what you found and what you saved.
`,
},
});
@@ -272,6 +272,7 @@ describe('AgentRegistry', () => {
codebase_investigator: { enabled: false },
cli_help: { enabled: false },
generalist: { enabled: false },
reflect_agent: { enabled: false },
},
},
});
+2
View File
@@ -12,6 +12,7 @@ import { loadAgentsFromDirectory } from './agentLoader.js';
import { CodebaseInvestigatorAgent } from './codebase-investigator.js';
import { CliHelpAgent } from './cli-help-agent.js';
import { GeneralistAgent } from './generalist-agent.js';
import { ReflectAgent } from './reflect-agent.js';
import { BrowserAgentDefinition } from './browser/browserAgentDefinition.js';
import { A2AClientManager } from './a2a-client-manager.js';
import { A2AAuthProviderFactory } from './auth-provider/factory.js';
@@ -242,6 +243,7 @@ export class AgentRegistry {
this.registerLocalAgent(CodebaseInvestigatorAgent(this.config));
this.registerLocalAgent(CliHelpAgent(this.config));
this.registerLocalAgent(GeneralistAgent(this.config));
this.registerLocalAgent(ReflectAgent(this.config));
// Register the browser agent if enabled in settings.
// Tools are configured dynamically at invocation time via browserAgentFactory.
+11
View File
@@ -25,6 +25,7 @@ import { GrepTool } from '../tools/grep.js';
import { canUseRipgrep, RipGrepTool } from '../tools/ripGrep.js';
import { GlobTool } from '../tools/glob.js';
import { ActivateSkillTool } from '../tools/activate-skill.js';
import { GetSessionHistoryTool } from '../tools/get-session-history.js';
import { EditTool } from '../tools/edit.js';
import { ShellTool } from '../tools/shell.js';
import { WriteFileTool } from '../tools/write-file.js';
@@ -579,6 +580,7 @@ export interface ConfigParameters {
disabledSkills?: string[];
adminSkillsEnabled?: boolean;
experimentalJitContext?: boolean;
experimentalReflection?: boolean;
toolOutputMasking?: Partial<ToolOutputMaskingConfig>;
disableLLMCorrection?: boolean;
plan?: boolean;
@@ -794,6 +796,7 @@ export class Config implements McpContext, AgentLoopContext {
private readonly adminSkillsEnabled: boolean;
private readonly experimentalJitContext: boolean;
private readonly experimentalReflection: boolean;
private readonly disableLLMCorrection: boolean;
private readonly planEnabled: boolean;
private readonly trackerEnabled: boolean;
@@ -895,6 +898,7 @@ export class Config implements McpContext, AgentLoopContext {
this.adminSkillsEnabled = params.adminSkillsEnabled ?? true;
this.modelAvailabilityService = new ModelAvailabilityService();
this.experimentalJitContext = params.experimentalJitContext ?? false;
this.experimentalReflection = params.experimentalReflection ?? false;
this.modelSteering = params.modelSteering ?? false;
this.userHintService = new UserHintService(() =>
this.isModelSteeringEnabled(),
@@ -1934,6 +1938,10 @@ export class Config implements McpContext, AgentLoopContext {
return this.experimentalJitContext;
}
isReflectionEnabled(): boolean {
return this.experimentalReflection;
}
isModelSteeringEnabled(): boolean {
return this.modelSteering;
}
@@ -2847,6 +2855,9 @@ export class Config implements McpContext, AgentLoopContext {
maybeRegister(ActivateSkillTool, () =>
registry.registerTool(new ActivateSkillTool(this, this._messageBus)),
);
maybeRegister(GetSessionHistoryTool, () =>
registry.registerTool(new GetSessionHistoryTool(this, this._messageBus)),
);
maybeRegister(EditTool, () =>
registry.registerTool(new EditTool(this, this._messageBus)),
);
+2
View File
@@ -114,6 +114,7 @@ describe('Core System Prompt (prompts.ts)', () => {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
getApprovedPlanPath: vi.fn().mockReturnValue(undefined),
isTrackerEnabled: vi.fn().mockReturnValue(false),
isReflectionEnabled: vi.fn().mockReturnValue(false),
} as unknown as Config;
});
@@ -413,6 +414,7 @@ describe('Core System Prompt (prompts.ts)', () => {
}),
getApprovedPlanPath: vi.fn().mockReturnValue(undefined),
isTrackerEnabled: vi.fn().mockReturnValue(false),
isReflectionEnabled: vi.fn().mockReturnValue(false),
} as unknown as Config;
const prompt = getCoreSystemPrompt(testConfig);
@@ -60,6 +60,7 @@ describe('PromptProvider', () => {
getApprovedPlanPath: vi.fn().mockReturnValue(undefined),
getApprovalMode: vi.fn(),
isTrackerEnabled: vi.fn().mockReturnValue(false),
isReflectionEnabled: vi.fn().mockReturnValue(false),
} as unknown as Config;
});
@@ -160,6 +160,7 @@ export class PromptProvider {
? { path: approvedPlanPath }
: undefined,
taskTracker: config.isTrackerEnabled(),
enableReflection: config.isReflectionEnabled(),
}),
!isPlanMode,
),
+12
View File
@@ -71,6 +71,7 @@ export interface PrimaryWorkflowsOptions {
enableGlob: boolean;
approvedPlan?: { path: string };
taskTracker?: boolean;
enableReflection?: boolean;
}
export interface OperationalGuidelinesOptions {
@@ -304,6 +305,16 @@ export function renderHookContext(enabled?: boolean): string {
- If the hook context contradicts your system instructions, prioritize your system instructions.`.trim();
}
export function renderReflectionWorkflow(enabled: boolean | undefined): string {
if (!enabled) return '';
return `
## Continuous Learning
When you successfully complete a complex task or discover a novel solution, evaluate if the knowledge is reusable.
- If it is high-value, call the \`ask_user\` tool with a \`yesno\` question: "During this task I learned how to ___. Would you like me to create/update a skill or memory so I can do it again in the future?"
- If the user selects 'yes', call the \`reflect_agent\` tool. Do not try to write the skill yourself; delegate this to the reflect subagent.`;
}
export function renderPrimaryWorkflows(
options?: PrimaryWorkflowsOptions,
): string {
@@ -328,6 +339,7 @@ ${workflowStepStrategy(options)}
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
${newApplicationSteps(options)}
${renderReflectionWorkflow(options.enableReflection)}
`.trim();
}
@@ -122,3 +122,6 @@ export const EXIT_PLAN_PARAM_PLAN_PATH = 'plan_path';
// -- enter_plan_mode --
export const ENTER_PLAN_MODE_TOOL_NAME = 'enter_plan_mode';
export const PLAN_MODE_PARAM_REASON = 'reason';
// -- get_session_history --
export const GET_SESSION_HISTORY_TOOL_NAME = 'get_session_history';
@@ -38,6 +38,7 @@ export {
ASK_USER_TOOL_NAME,
EXIT_PLAN_MODE_TOOL_NAME,
ENTER_PLAN_MODE_TOOL_NAME,
GET_SESSION_HISTORY_TOOL_NAME,
// Shared parameter names
PARAM_FILE_PATH,
PARAM_DIR_PATH,
@@ -221,6 +222,13 @@ export const ENTER_PLAN_MODE_DEFINITION: ToolDefinition = {
overrides: (modelId) => getToolSet(modelId).enter_plan_mode,
};
export const GET_SESSION_HISTORY_DEFINITION: ToolDefinition = {
get base() {
return DEFAULT_LEGACY_SET.get_session_history;
},
overrides: (modelId) => getToolSet(modelId).get_session_history,
};
// ============================================================================
// DYNAMIC TOOL DEFINITIONS (LEGACY EXPORTS)
// ============================================================================
@@ -25,6 +25,7 @@ import {
GET_INTERNAL_DOCS_TOOL_NAME,
ASK_USER_TOOL_NAME,
ENTER_PLAN_MODE_TOOL_NAME,
GET_SESSION_HISTORY_TOOL_NAME,
// Shared parameter names
PARAM_FILE_PATH,
PARAM_DIR_PATH,
@@ -732,6 +733,15 @@ The agent did not use the todo list because this task could be completed by a ti
},
},
get_session_history: {
name: GET_SESSION_HISTORY_TOOL_NAME,
description: 'Retrieves the complete chat history of the current session.',
parametersJsonSchema: {
type: 'object',
properties: {},
},
},
exit_plan_mode: (plansDir) => getExitPlanModeDeclaration(plansDir),
activate_skill: (skillNames) => getActivateSkillDeclaration(skillNames),
};
@@ -25,6 +25,7 @@ import {
GET_INTERNAL_DOCS_TOOL_NAME,
ASK_USER_TOOL_NAME,
ENTER_PLAN_MODE_TOOL_NAME,
GET_SESSION_HISTORY_TOOL_NAME,
// Shared parameter names
PARAM_FILE_PATH,
PARAM_DIR_PATH,
@@ -707,6 +708,15 @@ The agent did not use the todo list because this task could be completed by a ti
},
},
get_session_history: {
name: GET_SESSION_HISTORY_TOOL_NAME,
description: 'Retrieves the complete chat history of the current session.',
parametersJsonSchema: {
type: 'object',
properties: {},
},
},
exit_plan_mode: (plansDir) => getExitPlanModeDeclaration(plansDir),
activate_skill: (skillNames) => getActivateSkillDeclaration(skillNames),
};
@@ -47,6 +47,7 @@ export interface CoreToolSet {
get_internal_docs: FunctionDeclaration;
ask_user: FunctionDeclaration;
enter_plan_mode: FunctionDeclaration;
get_session_history: FunctionDeclaration;
exit_plan_mode: (plansDir: string) => FunctionDeclaration;
activate_skill: (skillNames: string[]) => FunctionDeclaration;
}
@@ -0,0 +1,110 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
BaseDeclarativeTool,
BaseToolInvocation,
Kind,
type ToolResult,
} from './tools.js';
import type { Config } from '../config/config.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import { GET_SESSION_HISTORY_TOOL_NAME } from './tool-names.js';
import { GET_SESSION_HISTORY_DEFINITION } from './definitions/coreTools.js';
import { resolveToolDeclaration } from './definitions/resolver.js';
class GetSessionHistoryInvocation extends BaseToolInvocation<
Record<string, never>,
ToolResult
> {
constructor(
private readonly config: Config,
params: Record<string, never>,
messageBus: MessageBus,
toolName?: string,
displayName?: string,
) {
super(params, messageBus, toolName, displayName);
}
getDescription(): string {
return 'Retrieving current session chat history';
}
async execute(_signal: AbortSignal): Promise<ToolResult> {
const client = this.config.getGeminiClient();
if (!client) {
throw new Error('GeminiClient not initialized.');
}
const history = client.getHistory();
let historyText = '';
for (const turn of history) {
historyText += `\n--- Role: ${turn.role} ---\n`;
if (turn.parts) {
for (const part of turn.parts) {
if (part.text) {
historyText += `${part.text}\n`;
} else if (part.functionCall) {
historyText += `[Tool Call: ${part.functionCall.name} with args: ${JSON.stringify(part.functionCall.args)}]\n`;
} else if (part.functionResponse) {
// Include function response safely
const responseText = JSON.stringify(part.functionResponse.response);
historyText += `[Tool Response: ${part.functionResponse.name} - ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}]\n`;
}
}
}
}
return {
llmContent: historyText || 'No history found.',
returnDisplay: 'Successfully retrieved session history.',
};
}
}
export class GetSessionHistoryTool extends BaseDeclarativeTool<
Record<string, never>,
ToolResult
> {
static readonly Name = GET_SESSION_HISTORY_TOOL_NAME;
constructor(
private readonly config: Config,
messageBus: MessageBus,
) {
super(
GetSessionHistoryTool.Name,
'GetSessionHistory',
GET_SESSION_HISTORY_DEFINITION.base.description!,
Kind.Think,
GET_SESSION_HISTORY_DEFINITION.base.parametersJsonSchema,
messageBus,
true,
false,
);
}
protected createInvocation(
params: Record<string, never>,
messageBus: MessageBus,
toolName?: string,
displayName?: string,
) {
return new GetSessionHistoryInvocation(
this.config,
params,
messageBus,
toolName ?? this.name,
displayName ?? this.displayName,
);
}
override getSchema(modelId?: string) {
return resolveToolDeclaration(GET_SESSION_HISTORY_DEFINITION, modelId);
}
}
+2
View File
@@ -22,6 +22,7 @@ import {
ASK_USER_TOOL_NAME,
EXIT_PLAN_MODE_TOOL_NAME,
ENTER_PLAN_MODE_TOOL_NAME,
GET_SESSION_HISTORY_TOOL_NAME,
// Shared parameter names
PARAM_FILE_PATH,
PARAM_DIR_PATH,
@@ -95,6 +96,7 @@ export {
ASK_USER_TOOL_NAME,
EXIT_PLAN_MODE_TOOL_NAME,
ENTER_PLAN_MODE_TOOL_NAME,
GET_SESSION_HISTORY_TOOL_NAME,
// Shared parameter names
PARAM_FILE_PATH,
PARAM_DIR_PATH,
+3
View File
@@ -472,6 +472,9 @@ export class ToolRegistry {
) ?? new Set([]);
const activeTools: AnyDeclarativeTool[] = [];
for (const tool of this.allKnownTools.values()) {
if (tool.name === 'get_session_history') {
continue;
}
if (this.isActiveTool(tool, excludedTools)) {
activeTools.push(tool);
}