-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix(agent-core-v2): re-remind subdirectory AGENTS.md after context compaction #3409
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,10 @@ import { | |
| import { profileKey } from '#/agent/profile/profileOps'; | ||
| import { IAgentStateService } from '#/agent/state/agentState'; | ||
| import { IAgentReminderService } from '#/features/reminder/reminderService'; | ||
| import type { | ||
| ContextInjectionContext, | ||
| ContextInjectionResult, | ||
| } from '#/features/reminder/types'; | ||
| import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; | ||
| import { IAgentToolExecutorService } from '#/agent/toolExecutor/toolExecutor'; | ||
| import type { ToolDidExecuteContext } from '#/agent/toolExecutor/toolHooks'; | ||
|
|
@@ -42,6 +46,10 @@ export const agentsMdReminderKnownKey = defineState<Set<string>>( | |
| 'agentsMdReminder.known', | ||
| () => new Set(), | ||
| ); | ||
| export const agentsMdReminderPendingKey = defineState<Set<string>>( | ||
| 'agentsMdReminder.pending', | ||
| () => new Set(), | ||
| ); | ||
| export const agentsMdReminderCwdKey = defineState<string | undefined>( | ||
| 'agentsMdReminder.cwd', | ||
| () => undefined as string | undefined, | ||
|
|
@@ -68,21 +76,26 @@ export class AgentAgentsMdReminderService | |
| @IBashParserService private readonly bashParser: IBashParserService, | ||
| @ITelemetryService private readonly telemetry: ITelemetryService, | ||
| @IEventDispatcher private readonly dispatcher: IEventDispatcher, | ||
| @IAgentStateService private readonly agentState: IAgentStateService, | ||
| @ISessionInstructionsProvider private readonly instructions: ISessionInstructionsProvider, | ||
| ) { | ||
| super(); | ||
| this.states.contributeState(agentsMdReminderKnownKey); | ||
| this.states.contributeState(agentsMdReminderPendingKey); | ||
| this.states.contributeState(agentsMdReminderCwdKey); | ||
| this.states.contributeState(agentsMdReminderSeededKey); | ||
| this._register( | ||
| this.reminder.register<readonly string[]>('agents_md', (context) => | ||
| this.injectReminder(context), | ||
| ), | ||
| ); | ||
| this._register( | ||
| this.instructions.onDidChange((changes) => { | ||
| this.announceChanged(changes); | ||
| }), | ||
| ); | ||
| this._register( | ||
| this.dispatcher.hooks.onDidRestore.register('agentsMdReminder', async (_ctx, next) => { | ||
| const profile = this.agentState.get(profileKey); | ||
| const profile = this.states.get(profileKey); | ||
| const paths = | ||
| profile.agentsMdPaths ?? extractAgentsMdPathsFromSystemPrompt(profile.systemPrompt); | ||
| this.seedInjected(paths, this.sessionContext.cwd); | ||
|
|
@@ -97,9 +110,12 @@ export class AgentAgentsMdReminderService | |
| } | ||
|
|
||
| seedInjected(paths: readonly string[], cwd: string): void { | ||
| const known = this.states.get(agentsMdReminderKnownKey); | ||
| const known = new Set(this.known); | ||
| for (const path of paths) known.add(normalize(path)); | ||
| this.states.set(agentsMdReminderKnownKey, new Set(known)); | ||
| this.states.set(agentsMdReminderKnownKey, known); | ||
| const pending = new Set(this.pending); | ||
| for (const path of paths) pending.delete(normalize(path)); | ||
| this.states.set(agentsMdReminderPendingKey, pending); | ||
| this.states.set(agentsMdReminderCwdKey, cwd); | ||
| this.states.set(agentsMdReminderSeededKey, true); | ||
| } | ||
|
|
@@ -116,8 +132,14 @@ export class AgentAgentsMdReminderService | |
| this.reminder.notify(changeReminderText(list), { | ||
| variant: 'agents_md_change', | ||
| }); | ||
| this.publishKnown( | ||
| list.filter((change) => change.action !== 'deleted').map((change) => change.path), | ||
| this.markKnown( | ||
| list.filter((change) => change.action === 'modified').map((change) => change.path), | ||
| ); | ||
| this.markPending( | ||
| list.filter((change) => change.action === 'created').map((change) => change.path), | ||
| ); | ||
|
Comment on lines
+138
to
140
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a discovered-but-unread AGENTS.md is deleted, this change announcement handles only created and modified paths, leaving the deleted path in AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L80-L82 Useful? React with 👍 / 👎. |
||
| this.markDeleted( | ||
| list.filter((change) => change.action === 'deleted').map((change) => change.path), | ||
| ); | ||
| } | ||
|
|
||
|
|
@@ -127,10 +149,30 @@ export class AgentAgentsMdReminderService | |
| return this.states.get(agentsMdReminderKnownKey); | ||
| } | ||
|
|
||
| private get pending(): Set<string> { | ||
| return this.states.get(agentsMdReminderPendingKey); | ||
| } | ||
|
|
||
| private get agentCwd(): string { | ||
| return this.states.get(agentsMdReminderCwdKey) ?? this.sessionContext.cwd; | ||
| } | ||
|
|
||
| private hasPath(path: string): boolean { | ||
| return this.known.has(path) || this.pending.has(path); | ||
| } | ||
|
|
||
| private injectReminder( | ||
| context: ContextInjectionContext<readonly string[]>, | ||
| ): ContextInjectionResult<readonly string[]> | undefined { | ||
| const known = this.known; | ||
| const pending = [...this.pending].filter((path) => !known.has(path)); | ||
| if (pending.length === 0) return undefined; | ||
| const covered = context.lastDisclosure ?? []; | ||
| const fresh = pending.filter((path) => !covered.includes(path)); | ||
| if (fresh.length === 0) return undefined; | ||
| return { content: reminderText(fresh), disclosure: pending }; | ||
| } | ||
|
|
||
| private async ensureSeeded(): Promise<void> { | ||
| if (this.states.get(agentsMdReminderSeededKey)) return; | ||
| const lease = this.runtime.acquire(['fs']); | ||
|
|
@@ -155,13 +197,13 @@ export class AgentAgentsMdReminderService | |
| const selfKnownSet = new Set(selfKnown); | ||
| for (const dir of dirs) { | ||
| for (const path of await this.probeDir(dir)) { | ||
| if (this.known.has(path) || this.claimed.has(path) || selfKnownSet.has(path)) continue; | ||
| if (this.hasPath(path) || this.claimed.has(path) || selfKnownSet.has(path)) continue; | ||
| this.claimed.add(path); | ||
| discovered.push(path); | ||
| } | ||
| } | ||
| if (discovered.length === 0) { | ||
| this.publishKnown(selfKnown); | ||
| this.markKnown(selfKnown); | ||
| return; | ||
| } | ||
| const properties: AgentsMdReminderShownEvent = { | ||
|
|
@@ -171,20 +213,45 @@ export class AgentAgentsMdReminderService | |
| trace_id: ctx.trace?.traceId, | ||
| }; | ||
| this.telemetry.track2('agents_md_reminder_shown', properties); | ||
| this.reminder.notify(reminderText(discovered), { | ||
| variant: 'agents_md', | ||
| }); | ||
| this.publishKnown([...selfKnown, ...discovered]); | ||
| this.markKnown(selfKnown); | ||
| this.markPending(discovered); | ||
| } catch {} finally { | ||
| for (const path of discovered) this.claimed.delete(path); | ||
| } | ||
| } | ||
|
|
||
| private publishKnown(paths: readonly string[]): void { | ||
| private markKnown(paths: readonly string[]): void { | ||
| if (paths.length === 0) return; | ||
| const known = new Set(this.known); | ||
| const pending = new Set(this.pending); | ||
| for (const path of paths) { | ||
| known.add(path); | ||
| pending.delete(path); | ||
| } | ||
| this.states.set(agentsMdReminderKnownKey, known); | ||
| this.states.set(agentsMdReminderPendingKey, pending); | ||
| } | ||
|
|
||
| private markPending(paths: readonly string[]): void { | ||
| if (paths.length === 0) return; | ||
| const merged = new Set(this.known); | ||
| for (const path of paths) merged.add(path); | ||
| this.states.set(agentsMdReminderKnownKey, merged); | ||
| const known = this.known; | ||
| const pending = new Set(this.pending); | ||
| for (const path of paths) { | ||
| if (!known.has(path)) pending.add(path); | ||
| } | ||
| this.states.set(agentsMdReminderPendingKey, pending); | ||
| } | ||
|
|
||
| private markDeleted(paths: readonly string[]): void { | ||
| if (paths.length === 0) return; | ||
| const known = new Set(this.known); | ||
| const pending = new Set(this.pending); | ||
| for (const path of paths) { | ||
| known.delete(path); | ||
| pending.delete(path); | ||
| } | ||
| this.states.set(agentsMdReminderKnownKey, known); | ||
| this.states.set(agentsMdReminderPendingKey, pending); | ||
| } | ||
|
|
||
| private targetDirs(ctx: ToolDidExecuteContext): { dirs: string[]; selfKnown: string[] } { | ||
|
|
@@ -272,7 +339,7 @@ export class AgentAgentsMdReminderService | |
| const found: string[] = []; | ||
| for (const chainDir of chain) { | ||
| const candidates = agentsMdCandidatePaths(chainDir); | ||
| if (candidates.every((candidate) => this.known.has(normalize(candidate)))) continue; | ||
| if (candidates.every((candidate) => this.hasPath(normalize(candidate)))) continue; | ||
| for (const path of await findAgentsMdInDir(deps, chainDir)) { | ||
| found.push(normalize(path)); | ||
| } | ||
|
|
@@ -309,7 +376,7 @@ function reminderText(paths: readonly string[]): string { | |
| return ( | ||
| 'The path(s) touched by a recent tool call are covered by AGENTS.md instruction file(s) that were not part of the injected instructions:\n' + | ||
| paths.map((path) => `- ${path}`).join('\n') + | ||
| '\nRead them before making changes in those directories. Each file is suggested at most once per agent.' | ||
| '\nRead them before making changes in those directories. Files may be suggested again after context compaction unless you have read them.' | ||
| ); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an unread pending AGENTS.md is modified, this moves it to
known, so compaction can drop both its discovery reminder and the one-off change notification without the registered reminder ever restoring either. Fresh evidence beyond the earlier created-file comment is thatWorkspaceInstructionsServicecoalesces same-path watcher events by overwritingpendingChangesatworkspaceInstructionsService.ts:130-132; a create followed by a modification within the debounce window is therefore delivered only asmodifiedand hits this path even though the file was never injected or read. Preserve pending provenance for modified paths unless they are already known.AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L80-L82
Useful? React with 👍 / 👎.