-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix(agent-core-v2): re-remind uninjected AGENTS.md on directory access after context loss #3462
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
7ec3e0c
422625e
dfcd98b
0a43040
059bbba
c9995bc
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 |
|---|---|---|
|
|
@@ -42,14 +42,12 @@ const AGENTS_MD_BASENAMES: ReadonlySet<string> = new Set<string>(AGENTS_MD_PLAIN | |
|
|
||
| const BASH_PARSE_OPTIONS = { timeoutMs: 20, maxNodes: 10_000 } as const; | ||
|
|
||
| const DISCOVERY_REMINDER_VARIANT = 'agents_md'; | ||
|
|
||
| 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, | ||
|
|
@@ -65,6 +63,10 @@ export class AgentAgentsMdReminderService | |
| { | ||
| declare readonly _serviceBrand: undefined; | ||
|
|
||
| private readonly remindQueue = new Set<string>(); | ||
| private readonly readRecently = new Set<string>(); | ||
| private readonly telemetryFired = new Set<string>(); | ||
|
|
||
| constructor( | ||
| @IAgentToolExecutorService toolExecutor: IAgentToolExecutorService, | ||
| @IAgentReminderService private readonly reminder: IAgentReminderService, | ||
|
|
@@ -80,11 +82,10 @@ export class AgentAgentsMdReminderService | |
| ) { | ||
| 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.reminder.register<readonly string[]>(DISCOVERY_REMINDER_VARIANT, (context) => | ||
| this.injectReminder(context), | ||
| ), | ||
| ); | ||
|
|
@@ -113,9 +114,7 @@ export class AgentAgentsMdReminderService | |
| const known = new Set(this.known); | ||
| for (const path of paths) known.add(normalize(path)); | ||
| 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); | ||
| for (const path of paths) this.remindQueue.delete(normalize(path)); | ||
| this.states.set(agentsMdReminderCwdKey, cwd); | ||
| this.states.set(agentsMdReminderSeededKey, true); | ||
| } | ||
|
|
@@ -135,42 +134,34 @@ export class AgentAgentsMdReminderService | |
| this.markKnown( | ||
| list.filter((change) => change.action === 'modified').map((change) => change.path), | ||
| ); | ||
| this.markPending( | ||
| list.filter((change) => change.action === 'created').map((change) => change.path), | ||
| ); | ||
| this.markDeleted( | ||
| list.filter((change) => change.action === 'deleted').map((change) => change.path), | ||
| ); | ||
| } | ||
|
|
||
| private readonly claimed = new Set<string>(); | ||
|
|
||
| private get known(): Set<string> { | ||
| 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 readRecently = new Set(this.readRecently); | ||
| this.readRecently.clear(); | ||
| const known = this.known; | ||
| const pending = [...this.pending].filter((path) => !known.has(path)); | ||
| if (pending.length === 0) return undefined; | ||
| const queued = [...this.remindQueue].filter( | ||
| (path) => !known.has(path) && !readRecently.has(path), | ||
| ); | ||
| this.remindQueue.clear(); | ||
| if (queued.length === 0) return undefined; | ||
| const covered = context.lastDisclosure ?? []; | ||
| const fresh = pending.filter((path) => !covered.includes(path)); | ||
| const fresh = queued.filter((path) => !covered.includes(path)); | ||
| if (fresh.length === 0) return undefined; | ||
| return { content: reminderText(fresh), disclosure: pending }; | ||
| return { content: reminderText(fresh), disclosure: [...covered, ...fresh] }; | ||
| } | ||
|
|
||
| private async ensureSeeded(): Promise<void> { | ||
|
|
@@ -190,68 +181,55 @@ export class AgentAgentsMdReminderService | |
|
|
||
| private async probeAndRemind(ctx: ToolDidExecuteContext): Promise<void> { | ||
| if (ctx.outcome !== 'executed') return; | ||
| const discovered: string[] = []; | ||
| try { | ||
| await this.ensureSeeded(); | ||
| const { dirs, selfKnown } = this.targetDirs(ctx); | ||
| const selfKnownSet = new Set(selfKnown); | ||
| const discovered: string[] = []; | ||
| for (const dir of dirs) { | ||
| for (const path of await this.probeDir(dir)) { | ||
| if (this.hasPath(path) || this.claimed.has(path) || selfKnownSet.has(path)) continue; | ||
| this.claimed.add(path); | ||
| if (this.known.has(path) || this.remindQueue.has(path) || selfKnownSet.has(path)) { | ||
| continue; | ||
| } | ||
| discovered.push(path); | ||
| } | ||
| } | ||
| if (discovered.length === 0) { | ||
| this.markKnown(selfKnown); | ||
| return; | ||
| for (const path of selfKnown) { | ||
| this.remindQueue.delete(path); | ||
| this.readRecently.add(path); | ||
| } | ||
| const properties: AgentsMdReminderShownEvent = { | ||
| turn_id: ctx.turnId, | ||
| tool_name: ctx.toolCall.name, | ||
| reminded_count: discovered.length, | ||
| trace_id: ctx.trace?.traceId, | ||
| }; | ||
| this.telemetry.track2('agents_md_reminder_shown', properties); | ||
| this.markKnown(selfKnown); | ||
| this.markPending(discovered); | ||
| } catch {} finally { | ||
| for (const path of discovered) this.claimed.delete(path); | ||
| } | ||
| if (discovered.length === 0) return; | ||
| const untracked = discovered.filter((path) => !this.telemetryFired.has(path)); | ||
| if (untracked.length > 0) { | ||
| const properties: AgentsMdReminderShownEvent = { | ||
| turn_id: ctx.turnId, | ||
| tool_name: ctx.toolCall.name, | ||
| reminded_count: untracked.length, | ||
| trace_id: ctx.trace?.traceId, | ||
| }; | ||
| this.telemetry.track2('agents_md_reminder_shown', properties); | ||
| for (const path of untracked) this.telemetryFired.add(path); | ||
| } | ||
| for (const path of discovered) this.remindQueue.add(path); | ||
| } catch {} | ||
| } | ||
|
|
||
| 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); | ||
| } | ||
| for (const path of paths) known.add(path); | ||
| this.states.set(agentsMdReminderKnownKey, known); | ||
| this.states.set(agentsMdReminderPendingKey, pending); | ||
| } | ||
|
|
||
| private markPending(paths: readonly string[]): void { | ||
| if (paths.length === 0) return; | ||
| 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.remindQueue.delete(path); | ||
| this.telemetryFired.delete(path); | ||
| } | ||
| this.states.set(agentsMdReminderKnownKey, known); | ||
|
Comment on lines
226
to
232
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 Useful? React with 👍 / 👎. |
||
| this.states.set(agentsMdReminderPendingKey, pending); | ||
| } | ||
|
|
||
| private targetDirs(ctx: ToolDidExecuteContext): { dirs: string[]; selfKnown: string[] } { | ||
|
|
@@ -339,7 +317,7 @@ export class AgentAgentsMdReminderService | |
| const found: string[] = []; | ||
| for (const chainDir of chain) { | ||
| const candidates = agentsMdCandidatePaths(chainDir); | ||
| if (candidates.every((candidate) => this.hasPath(normalize(candidate)))) continue; | ||
| if (candidates.every((candidate) => this.known.has(normalize(candidate)))) continue; | ||
| for (const path of await findAgentsMdInDir(deps, chainDir)) { | ||
| found.push(normalize(path)); | ||
| } | ||
|
|
||
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 a session is restored after an access tool result is persisted but before the next step head, this in-memory queue is reconstructed empty, so the applicable AGENTS.md reminder is lost and that access proceeds without its instructions. The package guide explicitly forbids deferred-delivery queues and classifies AGENTS.md discovery as a one-off
notify; route the discovery through that safe event path so delivery follows the persisted context lifecycle rather than service-local memory.AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L82-L82
Useful? React with 👍 / 👎.