feat(memory): add Auto Memory inbox flow with canonical-patch contract (#26338)

This commit is contained in:
Sandy Tao
2026-05-04 12:07:13 -07:00
committed by GitHub
parent 60a6a47d56
commit a7beb890d0
26 changed files with 4279 additions and 115 deletions
@@ -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();
});
});
+29 -14
View File
@@ -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: {
+15
View File
@@ -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.
*/