mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-14 13:53:02 -07:00
feat(memory): add Auto Memory inbox flow with canonical-patch contract (#26338)
This commit is contained in:
@@ -208,12 +208,20 @@ vi.mock('../config/scoped-config.js', async (importOriginal) => {
|
||||
...actual,
|
||||
runWithScopedWorkspaceContext: vi.fn(actual.runWithScopedWorkspaceContext),
|
||||
createScopedWorkspaceContext: vi.fn(actual.createScopedWorkspaceContext),
|
||||
runWithScopedAutoMemoryExtractionWriteAccess: vi.fn(
|
||||
actual.runWithScopedAutoMemoryExtractionWriteAccess,
|
||||
),
|
||||
runWithScopedMemoryInboxAccess: vi.fn(
|
||||
actual.runWithScopedMemoryInboxAccess,
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
import {
|
||||
runWithScopedWorkspaceContext,
|
||||
createScopedWorkspaceContext,
|
||||
runWithScopedAutoMemoryExtractionWriteAccess,
|
||||
runWithScopedMemoryInboxAccess,
|
||||
} from '../config/scoped-config.js';
|
||||
const mockedRunWithScopedWorkspaceContext = vi.mocked(
|
||||
runWithScopedWorkspaceContext,
|
||||
@@ -221,6 +229,12 @@ const mockedRunWithScopedWorkspaceContext = vi.mocked(
|
||||
const mockedCreateScopedWorkspaceContext = vi.mocked(
|
||||
createScopedWorkspaceContext,
|
||||
);
|
||||
const mockedRunWithScopedMemoryInboxAccess = vi.mocked(
|
||||
runWithScopedMemoryInboxAccess,
|
||||
);
|
||||
const mockedRunWithScopedAutoMemoryExtractionWriteAccess = vi.mocked(
|
||||
runWithScopedAutoMemoryExtractionWriteAccess,
|
||||
);
|
||||
|
||||
const MockedGeminiChat = vi.mocked(GeminiChat);
|
||||
const mockedGetDirectoryContextString = vi.mocked(getDirectoryContextString);
|
||||
@@ -422,6 +436,8 @@ describe('LocalAgentExecutor', () => {
|
||||
mockedLogAgentFinish.mockReset();
|
||||
mockedRunWithScopedWorkspaceContext.mockClear();
|
||||
mockedCreateScopedWorkspaceContext.mockClear();
|
||||
mockedRunWithScopedMemoryInboxAccess.mockClear();
|
||||
mockedRunWithScopedAutoMemoryExtractionWriteAccess.mockClear();
|
||||
mockedPromptIdContext.getStore.mockReset();
|
||||
mockedPromptIdContext.run.mockImplementation((_id, fn) => fn());
|
||||
|
||||
@@ -941,6 +957,52 @@ describe('LocalAgentExecutor', () => {
|
||||
expect(mockedRunWithScopedWorkspaceContext).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should use runWithScopedMemoryInboxAccess when memoryInboxAccess is set', async () => {
|
||||
const definition = createTestDefinition();
|
||||
definition.memoryInboxAccess = true;
|
||||
const executor = await LocalAgentExecutor.create(
|
||||
definition,
|
||||
mockConfig,
|
||||
onActivity,
|
||||
);
|
||||
|
||||
mockModelResponse([
|
||||
{
|
||||
name: COMPLETE_TASK_TOOL_NAME,
|
||||
args: { finalResult: 'done' },
|
||||
id: 'c1',
|
||||
},
|
||||
]);
|
||||
|
||||
await executor.run({ goal: 'test' }, signal);
|
||||
|
||||
expect(mockedRunWithScopedMemoryInboxAccess).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should use the extraction write scope when autoMemoryExtractionWriteAccess is set', async () => {
|
||||
const definition = createTestDefinition();
|
||||
definition.autoMemoryExtractionWriteAccess = true;
|
||||
const executor = await LocalAgentExecutor.create(
|
||||
definition,
|
||||
mockConfig,
|
||||
onActivity,
|
||||
);
|
||||
|
||||
mockModelResponse([
|
||||
{
|
||||
name: COMPLETE_TASK_TOOL_NAME,
|
||||
args: { finalResult: 'done' },
|
||||
id: 'c1',
|
||||
},
|
||||
]);
|
||||
|
||||
await executor.run({ goal: 'test' }, signal);
|
||||
|
||||
expect(
|
||||
mockedRunWithScopedAutoMemoryExtractionWriteAccess,
|
||||
).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should not use runWithScopedWorkspaceContext when workspaceDirectories is not set', async () => {
|
||||
const definition = createTestDefinition();
|
||||
const executor = await LocalAgentExecutor.create(
|
||||
@@ -962,6 +1024,10 @@ describe('LocalAgentExecutor', () => {
|
||||
|
||||
expect(mockedCreateScopedWorkspaceContext).not.toHaveBeenCalled();
|
||||
expect(mockedRunWithScopedWorkspaceContext).not.toHaveBeenCalled();
|
||||
expect(mockedRunWithScopedMemoryInboxAccess).not.toHaveBeenCalled();
|
||||
expect(
|
||||
mockedRunWithScopedAutoMemoryExtractionWriteAccess,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -77,6 +77,8 @@ import {
|
||||
import type { InjectionSource } from '../config/injectionService.js';
|
||||
import {
|
||||
createScopedWorkspaceContext,
|
||||
runWithScopedAutoMemoryExtractionWriteAccess,
|
||||
runWithScopedMemoryInboxAccess,
|
||||
runWithScopedWorkspaceContext,
|
||||
} from '../config/scoped-config.js';
|
||||
import { CompleteTaskTool } from '../tools/complete-task.js';
|
||||
@@ -529,21 +531,34 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
* @returns A promise that resolves to the agent's final output.
|
||||
*/
|
||||
async run(inputs: AgentInputs, signal: AbortSignal): Promise<OutputObject> {
|
||||
// If the agent definition declares additional workspace directories,
|
||||
// wrap execution in a scoped workspace context. All calls to
|
||||
// Config.getWorkspaceContext() within this scope will see the extended
|
||||
// directories, without mutating the shared Config.
|
||||
const dirs = this.definition.workspaceDirectories;
|
||||
if (dirs && dirs.length > 0) {
|
||||
const scopedCtx = createScopedWorkspaceContext(
|
||||
this.context.config.getWorkspaceContext(),
|
||||
dirs,
|
||||
);
|
||||
return runWithScopedWorkspaceContext(scopedCtx, () =>
|
||||
this.runInternal(inputs, signal),
|
||||
);
|
||||
const runWithWorkspaceScope = () => {
|
||||
// If the agent definition declares additional workspace directories,
|
||||
// wrap execution in a scoped workspace context. All calls to
|
||||
// Config.getWorkspaceContext() within this scope will see the extended
|
||||
// directories, without mutating the shared Config.
|
||||
const dirs = this.definition.workspaceDirectories;
|
||||
if (dirs && dirs.length > 0) {
|
||||
const scopedCtx = createScopedWorkspaceContext(
|
||||
this.context.config.getWorkspaceContext(),
|
||||
dirs,
|
||||
);
|
||||
return runWithScopedWorkspaceContext(scopedCtx, () =>
|
||||
this.runInternal(inputs, signal),
|
||||
);
|
||||
}
|
||||
return this.runInternal(inputs, signal);
|
||||
};
|
||||
|
||||
const runWithInboxScope = () =>
|
||||
this.definition.memoryInboxAccess
|
||||
? runWithScopedMemoryInboxAccess(runWithWorkspaceScope)
|
||||
: runWithWorkspaceScope();
|
||||
|
||||
if (this.definition.autoMemoryExtractionWriteAccess) {
|
||||
return runWithScopedAutoMemoryExtractionWriteAccess(runWithInboxScope);
|
||||
}
|
||||
return this.runInternal(inputs, signal);
|
||||
|
||||
return runWithInboxScope();
|
||||
}
|
||||
|
||||
private async runInternal(
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
GREP_TOOL_NAME,
|
||||
LS_TOOL_NAME,
|
||||
READ_FILE_TOOL_NAME,
|
||||
SHELL_TOOL_NAME,
|
||||
WRITE_FILE_TOOL_NAME,
|
||||
} from '../tools/tool-names.js';
|
||||
import { PREVIEW_GEMINI_FLASH_MODEL } from '../config/models.js';
|
||||
@@ -34,6 +35,8 @@ describe('SkillExtractionAgent', () => {
|
||||
expect(agent.name).toBe('confucius');
|
||||
expect(agent.displayName).toBe('Skill Extractor');
|
||||
expect(agent.modelConfig.model).toBe(PREVIEW_GEMINI_FLASH_MODEL);
|
||||
expect(agent.memoryInboxAccess).toBe(true);
|
||||
expect(agent.autoMemoryExtractionWriteAccess).toBe(true);
|
||||
expect(agent.toolConfig?.tools).toEqual(
|
||||
expect.arrayContaining([
|
||||
READ_FILE_TOOL_NAME,
|
||||
@@ -44,6 +47,7 @@ describe('SkillExtractionAgent', () => {
|
||||
GREP_TOOL_NAME,
|
||||
]),
|
||||
);
|
||||
expect(agent.toolConfig?.tools).not.toContain(SHELL_TOOL_NAME);
|
||||
});
|
||||
|
||||
it('should default to no skill unless recurrence and durability are proven', () => {
|
||||
@@ -69,6 +73,104 @@ describe('SkillExtractionAgent', () => {
|
||||
expect(prompt).toContain('cannot survive renaming the specific');
|
||||
});
|
||||
|
||||
it('should require all memory updates to go through .inbox/<kind>/*.patch for review', () => {
|
||||
const prompt = SkillExtractionAgent(
|
||||
skillsDir,
|
||||
sessionIndex,
|
||||
existingSkillsSummary,
|
||||
'/tmp/memory',
|
||||
).promptConfig.systemPrompt;
|
||||
|
||||
expect(prompt).toContain(
|
||||
'ALL memory updates are expressed as unified diff `.patch` files',
|
||||
);
|
||||
expect(prompt).toContain('EXACTLY ONE canonical patch file per kind');
|
||||
expect(prompt).toContain('extraction.patch');
|
||||
expect(prompt).not.toContain('MEMORY.patch');
|
||||
expect(prompt).not.toContain('verify-workflow.patch');
|
||||
expect(prompt).toContain('IMPORTANT — incremental updates');
|
||||
expect(prompt).toContain(
|
||||
'REWRITE that file by combining its existing hunks with your new',
|
||||
);
|
||||
expect(prompt).toContain('private ->');
|
||||
expect(prompt).toContain('global ->');
|
||||
expect(prompt).toContain(
|
||||
'the target MUST be exactly the single global personal memory',
|
||||
);
|
||||
expect(prompt).toContain('~/.gemini/GEMINI.md');
|
||||
expect(prompt).not.toContain('memory.md');
|
||||
expect(prompt).not.toContain('and siblings');
|
||||
expect(prompt).toContain(
|
||||
'Project/workspace shared instructions (GEMINI.md and similar files',
|
||||
);
|
||||
expect(prompt).toContain('MEMORY PATCH FORMAT (STRICT)');
|
||||
expect(prompt).toContain('--- /dev/null');
|
||||
expect(prompt).toContain('NEVER directly edit MEMORY.md');
|
||||
expect(prompt).toContain(
|
||||
'Every patch you write is held for /memory inbox review.',
|
||||
);
|
||||
expect(prompt).toContain('the user must approve each patch');
|
||||
|
||||
// The MEMORY.md-as-index discipline: sibling creations should pair with
|
||||
// a MEMORY.md update hunk; the inbox apply step auto-bundles a generic
|
||||
// pointer if the agent forgets, but the agent should write its own.
|
||||
expect(prompt).toContain('PRIVATE MEMORY: MEMORY.md IS THE INDEX');
|
||||
expect(prompt).toContain(
|
||||
'when you create a new sibling .md file, your patch SHOULD',
|
||||
);
|
||||
expect(prompt).toContain('a SECOND HUNK that updates MEMORY.md');
|
||||
expect(prompt).toContain('inbox apply step');
|
||||
expect(prompt).toContain('auto-bundle a generic pointer');
|
||||
|
||||
// Pointer paths must be ABSOLUTE — the runtime agent reads them directly.
|
||||
expect(prompt).toContain('IMPORTANT — pointer paths must be ABSOLUTE');
|
||||
expect(prompt).toContain('Always write the full path');
|
||||
// The example pointer in the prompt also uses the absolute path.
|
||||
expect(prompt).toContain(`+- See /tmp/memory/<topic>.md for`);
|
||||
});
|
||||
|
||||
it('surfaces existing inbox patches in the initial query when present', () => {
|
||||
const pendingInbox = [
|
||||
'## private (1)',
|
||||
'',
|
||||
'### extraction.patch',
|
||||
'```',
|
||||
'--- /dev/null',
|
||||
'+++ /tmp/memory/MEMORY.md',
|
||||
'@@ -0,0 +1,1 @@',
|
||||
'+- previously-extracted fact',
|
||||
'```',
|
||||
].join('\n');
|
||||
|
||||
const agentWithInbox = SkillExtractionAgent(
|
||||
skillsDir,
|
||||
sessionIndex,
|
||||
existingSkillsSummary,
|
||||
'/tmp/memory',
|
||||
pendingInbox,
|
||||
);
|
||||
const query = agentWithInbox.promptConfig.query ?? '';
|
||||
|
||||
expect(query).toContain('# Pending Memory Inbox');
|
||||
expect(query).toContain('extraction.patch');
|
||||
expect(query).toContain('previously-extracted fact');
|
||||
expect(query).toContain(
|
||||
'REWRITE that patch (overwrite the same path) with',
|
||||
);
|
||||
});
|
||||
|
||||
it('omits the pending inbox section when nothing is pending', () => {
|
||||
const agentEmpty = SkillExtractionAgent(
|
||||
skillsDir,
|
||||
sessionIndex,
|
||||
existingSkillsSummary,
|
||||
'/tmp/memory',
|
||||
'',
|
||||
);
|
||||
const query = agentEmpty.promptConfig.query ?? '';
|
||||
expect(query).not.toContain('# Pending Memory Inbox');
|
||||
});
|
||||
|
||||
it('should warn that session summaries are user-intent summaries, not workflow evidence', () => {
|
||||
const query = agent.promptConfig.query ?? '';
|
||||
|
||||
@@ -86,7 +188,10 @@ describe('SkillExtractionAgent', () => {
|
||||
'Only write a skill if the evidence shows a durable, recurring workflow',
|
||||
);
|
||||
expect(query).toContain(
|
||||
'If recurrence or future reuse is unclear, create no skill and explain why.',
|
||||
'Only write memory if it would clearly help a future session.',
|
||||
);
|
||||
expect(query).toContain(
|
||||
'If recurrence, durability, or future reuse is unclear, create no artifact and explain why.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
GREP_TOOL_NAME,
|
||||
LS_TOOL_NAME,
|
||||
READ_FILE_TOOL_NAME,
|
||||
SHELL_TOOL_NAME,
|
||||
WRITE_FILE_TOOL_NAME,
|
||||
} from '../tools/tool-names.js';
|
||||
import { PREVIEW_GEMINI_FLASH_MODEL } from '../config/models.js';
|
||||
@@ -21,20 +20,21 @@ import { PREVIEW_GEMINI_FLASH_MODEL } from '../config/models.js';
|
||||
const SkillExtractionSchema = z.object({
|
||||
response: z
|
||||
.string()
|
||||
.describe('A summary of the skills extracted or updated.'),
|
||||
.describe('A summary of the memories or skills extracted or updated.'),
|
||||
});
|
||||
|
||||
/**
|
||||
* Builds the system prompt for the skill extraction agent.
|
||||
*/
|
||||
function buildSystemPrompt(skillsDir: string): string {
|
||||
function buildSystemPrompt(skillsDir: string, memoryDir: string): string {
|
||||
return [
|
||||
'You are a Skill Extraction Agent.',
|
||||
'You are an Auto Memory Extraction Agent.',
|
||||
'',
|
||||
'Your job: analyze past conversation sessions and extract reusable skills that will help',
|
||||
'future agents work more efficiently. You write SKILL.md files to a specific directory.',
|
||||
'Your job: analyze past conversation sessions and extract durable memory candidates',
|
||||
'and reusable skills that will help future agents work more efficiently.',
|
||||
'',
|
||||
'The goal is to help future agents:',
|
||||
'- remember durable project facts, preferences, and workflow constraints',
|
||||
'- solve similar tasks with fewer tool calls and fewer reasoning tokens',
|
||||
'- reuse proven workflows and verification checklists',
|
||||
'- avoid known failure modes and landmines',
|
||||
@@ -48,8 +48,131 @@ function buildSystemPrompt(skillsDir: string): string {
|
||||
'- Evidence-based only: do not invent facts or claim verification that did not happen.',
|
||||
'- Redact secrets: never store tokens/keys/passwords; replace with [REDACTED].',
|
||||
'- Do not copy large tool outputs. Prefer compact summaries + exact error snippets.',
|
||||
` Write all files under this directory ONLY: ${skillsDir}`,
|
||||
' NEVER write files outside this directory. You may read session files from the paths provided in the index.',
|
||||
`- Write all files under this memory work directory ONLY: ${memoryDir}`,
|
||||
`- Reusable skill candidates go under: ${skillsDir}`,
|
||||
`- Reviewable memory candidates go under: ${memoryDir}/.inbox`,
|
||||
' NEVER write files outside the memory work directory. You may read session files from the paths provided in the index.',
|
||||
'',
|
||||
'============================================================',
|
||||
'MEMORY OUTPUTS',
|
||||
'============================================================',
|
||||
'',
|
||||
'ALL memory updates are expressed as unified diff `.patch` files. There is',
|
||||
`EXACTLY ONE canonical patch file per kind: ${memoryDir}/.inbox/<kind>/extraction.patch`,
|
||||
'where <kind> is one of:',
|
||||
'- private -> targets must live under the project memory directory',
|
||||
` (${memoryDir}). Use this for project-scoped private memory.`,
|
||||
'- global -> the target MUST be exactly the single global personal memory',
|
||||
' file ~/.gemini/GEMINI.md. No other files in ~/.gemini/ are',
|
||||
' writeable; sibling .md files do not exist for the global tier.',
|
||||
'',
|
||||
'IMPORTANT — incremental updates:',
|
||||
'- Before writing a new patch, check if "# Pending Memory Inbox" (above)',
|
||||
' already lists an `extraction.patch` for the same kind.',
|
||||
'- If yes: REWRITE that file by combining its existing hunks with your new',
|
||||
' ones (overwrite the same path with the merged multi-hunk patch). Do NOT',
|
||||
' create separate `topic-a.patch`, `topic-b.patch` files; everything goes',
|
||||
' in one canonical `extraction.patch` per kind.',
|
||||
'- If no: write a new `extraction.patch` with all your hunks.',
|
||||
'',
|
||||
'Project/workspace shared instructions (GEMINI.md and similar files under the',
|
||||
'project root) are NOT auto-extractable. They are managed by humans only; do',
|
||||
'not write patches that target files under the project root.',
|
||||
'',
|
||||
'NEVER directly edit MEMORY.md, GEMINI.md, ~/.gemini/GEMINI.md, settings,',
|
||||
'credentials, or any file outside the memory work directory. The only way to',
|
||||
'update memory is via a `.patch` file in the appropriate `.inbox/<kind>/` folder.',
|
||||
'',
|
||||
'Every patch you write is held for /memory inbox review. Nothing is applied',
|
||||
'automatically; the user must approve each patch before it touches active files.',
|
||||
'',
|
||||
'Private memory is for durable facts, preferences, decisions, and project context.',
|
||||
'Skills are only for reusable procedures. If both apply, avoid duplicating the same content.',
|
||||
'Default to no-op. Prefer 0-5 memory patches and 0-2 skills per run.',
|
||||
'',
|
||||
'============================================================',
|
||||
'PRIVATE MEMORY: MEMORY.md IS THE INDEX (CRITICAL)',
|
||||
'============================================================',
|
||||
'',
|
||||
`In <memoryDir> (${memoryDir}), only MEMORY.md is auto-loaded into future`,
|
||||
'agent contexts. Sibling .md files (e.g. verify-workflow.md, design-doc.md)',
|
||||
'are loaded ON DEMAND by the runtime agent via read_file ONLY when MEMORY.md',
|
||||
'references them.',
|
||||
'',
|
||||
'Therefore, when you create a new sibling .md file, your patch SHOULD',
|
||||
'include a SECOND HUNK that updates MEMORY.md to add a one-line pointer',
|
||||
'to the new file. The pointer is what makes the sibling discoverable to',
|
||||
'future agents.',
|
||||
'',
|
||||
'IMPORTANT — pointer paths must be ABSOLUTE. Future agents `read_file`',
|
||||
`directly off the pointer line, so the path must resolve without knowing`,
|
||||
`<memoryDir>. Always write the full path (${memoryDir}/<topic>.md), never`,
|
||||
'just the basename. The auto-bundle fallback also writes absolute paths.',
|
||||
'',
|
||||
'If you forget to include the MEMORY.md pointer, the inbox apply step',
|
||||
`will auto-bundle a generic pointer (\`- See ${memoryDir}/<name>.md for ...\`)`,
|
||||
'so the sibling is at least discoverable. But that auto-pointer is dumb —',
|
||||
'write the proper paired hunk yourself so MEMORY.md gets a meaningful',
|
||||
'summary.',
|
||||
'',
|
||||
'Correct shape for "create a new sibling" patch:',
|
||||
'',
|
||||
' --- /dev/null',
|
||||
` +++ ${memoryDir}/<topic>.md`,
|
||||
' @@ -0,0 +1,N @@',
|
||||
' +# <topic>',
|
||||
' +...',
|
||||
'',
|
||||
` --- ${memoryDir}/MEMORY.md`,
|
||||
` +++ ${memoryDir}/MEMORY.md`,
|
||||
' @@ -<line>,3 +<line>,4 @@',
|
||||
' <context>',
|
||||
' <context>',
|
||||
' <context>',
|
||||
` +- See ${memoryDir}/<topic>.md for <one-line summary>.`,
|
||||
'',
|
||||
'For brief facts (a few lines), prefer adding the entry directly to MEMORY.md',
|
||||
'as a single-hunk patch — no sibling file needed. Only spawn a sibling file',
|
||||
'when the content has substantial detail (multiple sections, procedures, etc.).',
|
||||
'',
|
||||
'============================================================',
|
||||
'MEMORY PATCH FORMAT (STRICT)',
|
||||
'============================================================',
|
||||
'',
|
||||
'Always read the target file first with read_file (or skip the read if the file',
|
||||
'definitely does not exist yet) so the patch context lines match exactly.',
|
||||
'',
|
||||
'Use one of these two unified diff shapes inside each `.patch` file:',
|
||||
'',
|
||||
'1. Update an existing file:',
|
||||
'',
|
||||
' --- /absolute/path/to/target.md',
|
||||
' +++ /absolute/path/to/target.md',
|
||||
' @@ -<oldStart>,<oldCount> +<newStart>,<newCount> @@',
|
||||
' <unchanged context line>',
|
||||
' -<removed line>',
|
||||
' +<added line>',
|
||||
' <unchanged context line>',
|
||||
'',
|
||||
'2. Create a brand-new file (no existing target):',
|
||||
'',
|
||||
' --- /dev/null',
|
||||
' +++ /absolute/path/to/new-target.md',
|
||||
' @@ -0,0 +1,<count> @@',
|
||||
' +<line 1>',
|
||||
' +<line 2>',
|
||||
'',
|
||||
'Patch rules:',
|
||||
'- Use the EXACT absolute file path in BOTH --- and +++ headers (NO `a/`/`b/` prefixes).',
|
||||
'- For updates, both headers must be the SAME absolute path.',
|
||||
'- Include 3 lines of context around each change for updates.',
|
||||
'- Line counts in @@ headers MUST be accurate.',
|
||||
'- One `.patch` file may include multiple hunks across multiple files in the same kind.',
|
||||
'- The patch FILENAME under .inbox/<kind>/ MUST be the canonical',
|
||||
' `extraction.patch`; the headers determine the actual target file(s).',
|
||||
'- Patches that fail validation or fail to apply cleanly are discarded silently.',
|
||||
"- The header path must resolve under the kind's allowed root (see above) or the",
|
||||
' patch will be rejected.',
|
||||
'',
|
||||
'============================================================',
|
||||
'NO-OP / MINIMUM SIGNAL GATE',
|
||||
@@ -212,8 +335,7 @@ function buildSystemPrompt(skillsDir: string): string {
|
||||
'2. If skills exist, read their SKILL.md files to understand what is already captured.',
|
||||
'3. Use activate_skill to load the "skill-creator" skill. Follow its design guidance',
|
||||
' (conciseness, progressive disclosure, frontmatter format, bundled resources) when',
|
||||
' writing SKILL.md files. You may also use its init_skill.cjs script to scaffold new',
|
||||
' skill directories and package_skill.cjs to validate finished skills.',
|
||||
' writing SKILL.md files.',
|
||||
' IMPORTANT: You are a background agent with no user interaction. Skip any interactive',
|
||||
' steps in the skill-creator guide (asking clarifying questions, requesting user feedback,',
|
||||
' installation prompts, iteration loops). Use only its format and quality guidance.',
|
||||
@@ -228,15 +350,19 @@ function buildSystemPrompt(skillsDir: string): string {
|
||||
'7. For each candidate, verify it meets ALL criteria. Before writing, make sure you can',
|
||||
' state: future trigger, evidence sessions, recurrence signal, validation signal, and',
|
||||
' why it is not generic.',
|
||||
'8. Write new SKILL.md files or update existing ones in your directory.',
|
||||
' Use run_shell_command to run init_skill.cjs for scaffolding and package_skill.cjs for validation.',
|
||||
' For skills that live OUTSIDE your directory, write a .patch file instead (see UPDATING EXISTING SKILLS).',
|
||||
'9. Write COMPLETE files — never partially update a SKILL.md.',
|
||||
'8. For memory candidates: read the target file first (or confirm it does not exist),',
|
||||
' then write a `.patch` file under the appropriate .inbox/<kind>/ directory using',
|
||||
' the format in MEMORY PATCH FORMAT. Prefer updating existing memory files over',
|
||||
' duplicating facts. Keep patches small and focused.',
|
||||
'9. Write new SKILL.md files or update existing ones in your skills directory.',
|
||||
' Use write_file/edit directly; shell commands are intentionally unavailable in this background flow.',
|
||||
' For skills that live OUTSIDE your skills directory, write a `.patch` file there instead (see UPDATING EXISTING SKILLS).',
|
||||
'10. Write COMPLETE SKILL.md files — never partially update a SKILL.md.',
|
||||
'',
|
||||
'IMPORTANT: Do NOT read every session. Only read sessions whose summaries suggest a',
|
||||
'repeated pattern or a stable recurring repo workflow worth investigating. Most runs',
|
||||
'should read 0-3 sessions and create 0 skills.',
|
||||
'Do not explore the codebase. Work only with the session index, session files, and the skills directory.',
|
||||
'should read 0-3 sessions and create few or no artifacts.',
|
||||
'Do not explore the codebase. Work only with the session index, session files, and the memory work directory.',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@@ -253,12 +379,20 @@ export const SkillExtractionAgent = (
|
||||
skillsDir: string,
|
||||
sessionIndex: string,
|
||||
existingSkillsSummary: string,
|
||||
memoryDir: string = skillsDir.replace(/[/\\]skills$/, ''),
|
||||
/**
|
||||
* Snapshot of the current memory inbox state, formatted for the agent's
|
||||
* initial context. Lets the agent see what's already pending so it can
|
||||
* extend or rewrite existing canonical patches instead of accumulating
|
||||
* many small ones across sessions. Empty string = nothing pending.
|
||||
*/
|
||||
pendingInboxSummary: string = '',
|
||||
): LocalAgentDefinition<typeof SkillExtractionSchema> => ({
|
||||
kind: 'local',
|
||||
name: 'confucius',
|
||||
displayName: 'Skill Extractor',
|
||||
description:
|
||||
'Extracts reusable skills from past conversation sessions and writes them as SKILL.md files.',
|
||||
'Extracts durable memories and reusable skills from past conversation sessions.',
|
||||
inputConfig: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
@@ -279,6 +413,8 @@ export const SkillExtractionAgent = (
|
||||
modelConfig: {
|
||||
model: PREVIEW_GEMINI_FLASH_MODEL,
|
||||
},
|
||||
memoryInboxAccess: true,
|
||||
autoMemoryExtractionWriteAccess: true,
|
||||
toolConfig: {
|
||||
tools: [
|
||||
ACTIVATE_SKILL_TOOL_NAME,
|
||||
@@ -288,7 +424,6 @@ export const SkillExtractionAgent = (
|
||||
LS_TOOL_NAME,
|
||||
GLOB_TOOL_NAME,
|
||||
GREP_TOOL_NAME,
|
||||
SHELL_TOOL_NAME,
|
||||
],
|
||||
},
|
||||
get promptConfig() {
|
||||
@@ -298,6 +433,23 @@ export const SkillExtractionAgent = (
|
||||
contextParts.push(`# Existing Skills\n\n${existingSkillsSummary}`);
|
||||
}
|
||||
|
||||
if (pendingInboxSummary && pendingInboxSummary.trim().length > 0) {
|
||||
contextParts.push(
|
||||
[
|
||||
'# Pending Memory Inbox',
|
||||
'',
|
||||
'The following `.patch` files already exist in the memory inbox',
|
||||
'awaiting user review. If your new findings overlap with one of',
|
||||
'these patches, REWRITE that patch (overwrite the same path) with',
|
||||
'the merged content rather than creating a new patch file. Use the',
|
||||
'canonical filename `extraction.patch` per kind for any new patch',
|
||||
'so the inbox stays consolidated.',
|
||||
'',
|
||||
pendingInboxSummary,
|
||||
].join('\n'),
|
||||
);
|
||||
}
|
||||
|
||||
contextParts.push(
|
||||
[
|
||||
'# Session Index',
|
||||
@@ -326,8 +478,8 @@ export const SkillExtractionAgent = (
|
||||
.replace(/\$\{(\w+)\}/g, '{$1}');
|
||||
|
||||
return {
|
||||
systemPrompt: buildSystemPrompt(skillsDir),
|
||||
query: `${initialContext}\n\nAnalyze the session index above. Session summaries describe user intent; optional workflow hints describe likely procedural traces. Use workflow hints for routing, then read sessions that suggest repeated workflows using read_file to verify recurrence from transcript evidence. Only write a skill if the evidence shows a durable, recurring workflow or a stable recurring repo procedure. If recurrence or future reuse is unclear, create no skill and explain why.`,
|
||||
systemPrompt: buildSystemPrompt(skillsDir, memoryDir),
|
||||
query: `${initialContext}\n\nAnalyze the session index above. Session summaries describe user intent; optional workflow hints describe likely procedural traces. Use workflow hints for routing, then read sessions that suggest durable memory or repeated workflows using read_file to verify from transcript evidence. Only write a skill if the evidence shows a durable, recurring workflow or a stable recurring repo procedure. Only write memory if it would clearly help a future session. If recurrence, durability, or future reuse is unclear, create no artifact and explain why. If no skill is justified, create no skill and explain why.`,
|
||||
};
|
||||
},
|
||||
runConfig: {
|
||||
|
||||
@@ -229,6 +229,21 @@ export interface LocalAgentDefinition<
|
||||
*/
|
||||
workspaceDirectories?: string[];
|
||||
|
||||
/**
|
||||
* Allows this agent to access the canonical auto-memory inbox patch files
|
||||
* under `<projectMemoryDir>/.inbox/{private,global}/extraction.patch`.
|
||||
* This is intentionally narrow so the main session cannot bypass review by
|
||||
* writing arbitrary inbox patches.
|
||||
*/
|
||||
memoryInboxAccess?: boolean;
|
||||
|
||||
/**
|
||||
* Restricts write validation for this agent to extracted skill artifacts and
|
||||
* canonical auto-memory inbox patch files. Used by the background
|
||||
* auto-memory extractor so active memory files cannot be edited directly.
|
||||
*/
|
||||
autoMemoryExtractionWriteAccess?: boolean;
|
||||
|
||||
/**
|
||||
* Optional inline MCP servers for this agent.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user