Repository navigation
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 2 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 |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Re-show directory AGENTS.md reminders when the agent next touches the directory after context compaction, instead of right after compaction. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,8 @@ import { ITelemetryService } from '#/app/telemetry/telemetry'; | |
| import type { IHostFileSystem } from '#/os/interface/hostFileSystem'; | ||
| import type { HostFsChange } from '#/os/interface/hostFsWatch'; | ||
| import { IAgentRuntimeService } from '#/agent/runtimeBinding/agentRuntime'; | ||
| import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory'; | ||
| import { IAgentLoopService } from '#/agent/loop/loop'; | ||
| import { ISessionContext } from '#/session/sessionContext/sessionContext'; | ||
| import { ISessionInstructionsProvider } from '#/session/sessionInstructions/instructionsProvider'; | ||
| import { normalizeUserPath } from '#/tool/path-access'; | ||
|
|
@@ -26,10 +28,6 @@ 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,14 +40,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, | ||
|
|
@@ -68,6 +64,8 @@ export class AgentAgentsMdReminderService | |
| constructor( | ||
| @IAgentToolExecutorService toolExecutor: IAgentToolExecutorService, | ||
| @IAgentReminderService private readonly reminder: IAgentReminderService, | ||
| @IAgentContextMemoryService private readonly contextMemory: IAgentContextMemoryService, | ||
| @IAgentLoopService loop: IAgentLoopService, | ||
| @IAgentScopeContext private readonly scopeContext: IAgentScopeContext, | ||
| @IAgentStateService private readonly states: IAgentStateService, | ||
| @ISessionContext private readonly sessionContext: ISessionContext, | ||
|
|
@@ -80,19 +78,19 @@ 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.injectReminder(context), | ||
| ), | ||
| ); | ||
| this._register( | ||
| this.instructions.onDidChange((changes) => { | ||
| this.announceChanged(changes); | ||
| }), | ||
| ); | ||
| this._register( | ||
| loop.hooks.onWillBeginStep.register('agentsMdReminder', async (_ctx, next) => { | ||
| this.claimed.clear(); | ||
| await next(); | ||
| }), | ||
| ); | ||
| this._register( | ||
| this.dispatcher.hooks.onDidRestore.register('agentsMdReminder', async (_ctx, next) => { | ||
| const profile = this.states.get(profileKey); | ||
|
|
@@ -113,9 +111,6 @@ 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); | ||
| this.states.set(agentsMdReminderCwdKey, cwd); | ||
| this.states.set(agentsMdReminderSeededKey, true); | ||
| } | ||
|
|
@@ -135,9 +130,6 @@ 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), | ||
| ); | ||
|
|
@@ -149,28 +141,22 @@ 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 inContextCoverage(): Set<string> { | ||
| const covered = new Set<string>(); | ||
| for (const message of this.contextMemory.get()) { | ||
| if (message.origin?.kind !== 'injection') continue; | ||
| if (message.origin.variant !== DISCOVERY_REMINDER_VARIANT) continue; | ||
| const disclosure = message.origin.disclosure; | ||
| if (!Array.isArray(disclosure)) continue; | ||
| for (const path of disclosure) { | ||
| if (typeof path === 'string') covered.add(normalize(path)); | ||
| } | ||
| } | ||
| return covered; | ||
| } | ||
|
|
||
| private async ensureSeeded(): Promise<void> { | ||
|
|
@@ -191,67 +177,54 @@ export class AgentAgentsMdReminderService | |
| private async probeAndRemind(ctx: ToolDidExecuteContext): Promise<void> { | ||
| if (ctx.outcome !== 'executed') return; | ||
| const discovered: string[] = []; | ||
| let keepClaims = false; | ||
| try { | ||
| await this.ensureSeeded(); | ||
| const { dirs, selfKnown } = this.targetDirs(ctx); | ||
| const selfKnownSet = new Set(selfKnown); | ||
| 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; | ||
| if (this.known.has(path) || this.claimed.has(path) || selfKnownSet.has(path)) continue; | ||
| this.claimed.add(path); | ||
| discovered.push(path); | ||
| } | ||
| } | ||
| if (discovered.length === 0) { | ||
| this.markKnown(selfKnown); | ||
| return; | ||
| } | ||
| this.markKnown(selfKnown); | ||
| if (discovered.length === 0) return; | ||
| const covered = this.inContextCoverage(); | ||
| const fresh = discovered.filter((path) => !covered.has(path)); | ||
| if (fresh.length === 0) return; | ||
| const properties: AgentsMdReminderShownEvent = { | ||
| turn_id: ctx.turnId, | ||
| tool_name: ctx.toolCall.name, | ||
| reminded_count: discovered.length, | ||
| reminded_count: fresh.length, | ||
| trace_id: ctx.trace?.traceId, | ||
| }; | ||
| this.telemetry.track2('agents_md_reminder_shown', properties); | ||
| this.markKnown(selfKnown); | ||
| this.markPending(discovered); | ||
| this.reminder.notify(reminderText(fresh), { | ||
| variant: DISCOVERY_REMINDER_VARIANT, | ||
| disclosure: fresh, | ||
| }); | ||
|
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 parallel tool calls touch a directory and read its AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L82-L82 Useful? React with 👍 / 👎. |
||
| keepClaims = true; | ||
| } catch {} finally { | ||
| for (const path of discovered) this.claimed.delete(path); | ||
| if (!keepClaims) { | ||
| for (const path of discovered) this.claimed.delete(path); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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); | ||
| } | ||
| for (const path of paths) known.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 +312,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 multi-tool step contains staggered calls that touch the same directory, the first
notify()is deferred by the context fold while another tool result is still pending, so it is absent fromcontextMemory.get()here. Because the first hook then removes its temporaryclaimedentries, the slower sibling call sees neither coverage nor a claim and emits the same one-off AGENTS.md reminder and telemetry event again. Retain discovery coverage until pending sibling tools settle, or otherwise include deferred notifications in this check.AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L80-L82
Useful? React with 👍 / 👎.