Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion 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: 80 keys)
// Index (App: 0 keys · Workspace: 6 keys · Session: 9 keys · Agent: 81 keys)
// App
// Workspace
// workspaceDirs.ephemeralDirs src/workspace/workspaceDirs/workspaceDirsService.ts
Expand Down Expand Up @@ -55,6 +55,7 @@
// 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 @@ -1038,6 +1039,7 @@ 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 @@ -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';
Expand All @@ -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,
Expand All @@ -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);
Expand All @@ -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);
}
Expand All @@ -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),
);
Comment on lines +135 to +137

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 Keep modified pending files eligible for re-injection

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 that WorkspaceInstructionsService coalesces same-path watcher events by overwriting pendingChanges at workspaceInstructionsService.ts:130-132; a create followed by a modification within the debounce window is therefore delivered only as modified and 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 👍 / 👎.

this.markPending(
list.filter((change) => change.action === 'created').map((change) => change.path),
);
Comment on lines +138 to 140

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 files from pending reminders

When a discovered-but-unread AGENTS.md is deleted, this change announcement handles only created and modified paths, leaving the deleted path in agentsMdReminder.pending. After compaction, /clear, or undo removes the prior reminder, injectReminder() will therefore tell the model to read a file that no longer exists; clear deleted paths from the pending set when processing the watcher event.

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

Expand All @@ -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']);
Expand All @@ -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 = {
Expand All @@ -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[] } {
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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.'
);
}

Expand Down
4 changes: 2 additions & 2 deletions packages/agent-core-v2/src/app/telemetry/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -942,10 +942,10 @@ export const telemetryEventDefinitions = {
}),
agents_md_reminder_shown: defineAgentTelemetryEvent<AgentsMdReminderShownEvent>({
owner: 'kimi-code',
comment: 'An AGENTS.md discovery reminder is appended to a tool result.',
comment: 'An AGENTS.md discovery reminder is queued for context injection 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 result carried the reminder',
tool_name: 'Registered tool name whose execution discovered the file',
reminded_count: 'Number of AGENTS.md paths listed in the reminder',
trace_id:
'Trace id of the LLM request that produced the tool call; absent for non-Kimi protocols',
Expand Down
Loading
Loading