diff --git a/.gemini/settings.json b/.gemini/settings.json index 1a4c889066..286a00fc37 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -2,7 +2,8 @@ "experimental": { "plan": true, "extensionReloading": true, - "modelSteering": true + "modelSteering": true, + "reflection": true }, "general": { "devtools": true diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index a8c85975e9..2f905e8701 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -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'], diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index bd1f9d82a4..b327cfce5c 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -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', diff --git a/packages/core/src/agents/reflect-agent.ts b/packages/core/src/agents/reflect-agent.ts new file mode 100644 index 0000000000..2229ac969e --- /dev/null +++ b/packages/core/src/agents/reflect-agent.ts @@ -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 => ({ + 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. +`, + }, +}); diff --git a/packages/core/src/agents/registry.test.ts b/packages/core/src/agents/registry.test.ts index edae478f2a..928d8d4726 100644 --- a/packages/core/src/agents/registry.test.ts +++ b/packages/core/src/agents/registry.test.ts @@ -272,6 +272,7 @@ describe('AgentRegistry', () => { codebase_investigator: { enabled: false }, cli_help: { enabled: false }, generalist: { enabled: false }, + reflect_agent: { enabled: false }, }, }, }); diff --git a/packages/core/src/agents/registry.ts b/packages/core/src/agents/registry.ts index bf7e669150..19930f7c38 100644 --- a/packages/core/src/agents/registry.ts +++ b/packages/core/src/agents/registry.ts @@ -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. diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index f615564533..5c7d9e282a 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -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; 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)), ); diff --git a/packages/core/src/core/prompts.test.ts b/packages/core/src/core/prompts.test.ts index ba9b0ec93b..122d34b863 100644 --- a/packages/core/src/core/prompts.test.ts +++ b/packages/core/src/core/prompts.test.ts @@ -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); diff --git a/packages/core/src/prompts/promptProvider.test.ts b/packages/core/src/prompts/promptProvider.test.ts index 2d96dee7ef..af52e9d710 100644 --- a/packages/core/src/prompts/promptProvider.test.ts +++ b/packages/core/src/prompts/promptProvider.test.ts @@ -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; }); diff --git a/packages/core/src/prompts/promptProvider.ts b/packages/core/src/prompts/promptProvider.ts index 01dbd8d4d4..1c1f7a7274 100644 --- a/packages/core/src/prompts/promptProvider.ts +++ b/packages/core/src/prompts/promptProvider.ts @@ -160,6 +160,7 @@ export class PromptProvider { ? { path: approvedPlanPath } : undefined, taskTracker: config.isTrackerEnabled(), + enableReflection: config.isReflectionEnabled(), }), !isPlanMode, ), diff --git a/packages/core/src/prompts/snippets.ts b/packages/core/src/prompts/snippets.ts index 3b3334f96b..7de4817fa3 100644 --- a/packages/core/src/prompts/snippets.ts +++ b/packages/core/src/prompts/snippets.ts @@ -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(); } diff --git a/packages/core/src/tools/definitions/base-declarations.ts b/packages/core/src/tools/definitions/base-declarations.ts index b39dc42286..d33842c661 100644 --- a/packages/core/src/tools/definitions/base-declarations.ts +++ b/packages/core/src/tools/definitions/base-declarations.ts @@ -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'; diff --git a/packages/core/src/tools/definitions/coreTools.ts b/packages/core/src/tools/definitions/coreTools.ts index b5121ca5d2..720a813769 100644 --- a/packages/core/src/tools/definitions/coreTools.ts +++ b/packages/core/src/tools/definitions/coreTools.ts @@ -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) // ============================================================================ diff --git a/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts b/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts index 3309fcc5ba..e6e3b5e1a5 100644 --- a/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts +++ b/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts @@ -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), }; diff --git a/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts b/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts index 2c0375baa3..2ad5de5de8 100644 --- a/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts +++ b/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts @@ -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), }; diff --git a/packages/core/src/tools/definitions/types.ts b/packages/core/src/tools/definitions/types.ts index a9bd3d85d7..ecc4d9f6d4 100644 --- a/packages/core/src/tools/definitions/types.ts +++ b/packages/core/src/tools/definitions/types.ts @@ -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; } diff --git a/packages/core/src/tools/get-session-history.ts b/packages/core/src/tools/get-session-history.ts new file mode 100644 index 0000000000..b04a601a07 --- /dev/null +++ b/packages/core/src/tools/get-session-history.ts @@ -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, + ToolResult +> { + constructor( + private readonly config: Config, + params: Record, + messageBus: MessageBus, + toolName?: string, + displayName?: string, + ) { + super(params, messageBus, toolName, displayName); + } + + getDescription(): string { + return 'Retrieving current session chat history'; + } + + async execute(_signal: AbortSignal): Promise { + 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, + 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, + 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); + } +} diff --git a/packages/core/src/tools/tool-names.ts b/packages/core/src/tools/tool-names.ts index fcdcbd6df6..4893610c87 100644 --- a/packages/core/src/tools/tool-names.ts +++ b/packages/core/src/tools/tool-names.ts @@ -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, diff --git a/packages/core/src/tools/tool-registry.ts b/packages/core/src/tools/tool-registry.ts index 69695877c2..eb4a37db88 100644 --- a/packages/core/src/tools/tool-registry.ts +++ b/packages/core/src/tools/tool-registry.ts @@ -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); }