Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/agents-md-access-reminder.md
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.
4 changes: 1 addition & 3 deletions packages/agent-core-v2/docs/state-manifest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
// references become '(circular)', and class instances collapse to a '(ClassName)'
// marker — the wire shape of an entry is the JSON projection of the type here.
//
// Index (App: 0 keys · Workspace: 6 keys · Session: 9 keys · Agent: 83 keys)
// Index (App: 0 keys · Workspace: 6 keys · Session: 9 keys · Agent: 82 keys)
// App
// Workspace
// workspaceDirs.ephemeralDirs src/workspace/workspaceDirs/workspaceDirsService.ts
Expand Down Expand Up @@ -55,7 +55,6 @@
// agentPlugin.sessionStartRefreshPending src/agent/plugin/agentPluginService.ts
// agentsMdReminder.cwd src/agent/agentsMdReminder/agentsMdReminderService.ts
// agentsMdReminder.known src/agent/agentsMdReminder/agentsMdReminderService.ts
// agentsMdReminder.pending src/agent/agentsMdReminder/agentsMdReminderService.ts
// agentsMdReminder.seeded src/agent/agentsMdReminder/agentsMdReminderService.ts
// contextMemory src/agent/contextMemory/contextOps.ts
// contextProjector.lastRepairSignature src/agent/contextProjector/contextProjectorService.ts
Expand Down Expand Up @@ -1041,7 +1040,6 @@ export interface AgentStateSnapshot {
// src/agent/agentsMdReminder/agentsMdReminderService.ts
'agentsMdReminder.cwd': string | undefined;
'agentsMdReminder.known': Set<string>;
'agentsMdReminder.pending': Set<string>;
'agentsMdReminder.seeded': boolean;
// src/agent/contextMemory/contextOps.ts
// replayable · durable · undoable — folds: ContextAppendMessage, ContextAppendLoopEvent, ContextClear, ContextApplyCompaction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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);
Expand All @@ -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);
}
Expand All @@ -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),
);
Expand All @@ -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> {
Expand All @@ -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));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve deduplication across parallel tool completions

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 from contextMemory.get() here. Because the first hook then removes its temporary claimed entries, 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 👍 / 👎.

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,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Suppress reminders when a sibling reads AGENTS.md directly

When parallel tool calls touch a directory and read its AGENTS.md directly, a faster sibling can reach this notify() first; the loop-event fold defers that notification until pending tool results settle, but the later direct read only updates known and cannot cancel the queued reminder. The model therefore receives a misleading “read this file” reminder, and duplicate telemetry, even though the file was already read. Revalidate known after sibling completions settle or retract the pending notification when a direct read marks the path known.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove deleted paths from the reminder queue

When a discovered AGENTS.md is deleted after a tool hook queues it but before the next step head—for example, while a slower sibling tool is still running—markDeleted now removes it only from known. The path remains in remindQueue, so injectReminder subsequently tells the model to read a file that no longer exists; remove the normalized deleted paths from the queue here as well.

Useful? React with 👍 / 👎.

this.states.set(agentsMdReminderPendingKey, pending);
}

private targetDirs(ctx: ToolDidExecuteContext): { dirs: string[]; selfKnown: string[] } {
Expand Down Expand Up @@ -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));
}
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/app/telemetry/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -958,7 +958,7 @@ export const telemetryEventDefinitions = {
}),
agents_md_reminder_shown: defineAgentTelemetryEvent<AgentsMdReminderShownEvent>({
owner: 'kimi-code',
comment: 'An AGENTS.md discovery reminder is queued for context injection after a tool call.',
comment: 'An AGENTS.md discovery reminder is appended to the context after a tool call.',
properties: {
turn_id: 'Per-agent turn index (main or subagent); pair with agent_id to locate a turn within a session',
tool_name: 'Registered tool name whose execution discovered the file',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ function appendReminder(
kind: 'injection',
variant: notification.variant,
ownerPromptId: notification.ownerPromptId,
disclosure: notification.disclosure,
},
});
}
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core-v2/src/features/reminder/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ export interface ReminderRegistration extends IDisposable {}
export interface ReminderNotification {
readonly variant: string;
readonly ownerPromptId?: string;
readonly disclosure?: unknown;
}
Loading
Loading