refactor(agent-core-v2): remove the agents_md context injection

This commit is contained in:
7Sageer 2026-07-30 16:07:13 +08:00
parent 9a73fe0c3b
commit fa8cd4ce58
15 changed files with 9 additions and 638 deletions

View file

@ -23,7 +23,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 (Session: 28 keys · Agent: 70 keys)
// Index (Session: 28 keys · Agent: 69 keys)
// Session
// cron.inFlight src/session/cron/sessionCronServiceImpl.ts
// cron.lastSeenAt src/session/cron/sessionCronServiceImpl.ts
@ -59,7 +59,6 @@
// activityView.lastTurn src/agent/activityView/activityViewService.ts
// activityView.lifecycle src/agent/activityView/activityViewService.ts
// activityView.turn src/agent/activityView/activityViewService.ts
// agentsMdReminder.seed src/agent/profile/agentsMdReminderService.ts
// contextInjector.isNewTurn src/agent/contextInjector/contextInjectorService.ts
// contextProjector.lastRepairSignature src/agent/contextProjector/contextProjectorService.ts
// contextSize.lastEmittedTokens src/agent/contextSize/contextSizeService.ts
@ -204,7 +203,6 @@ export interface SessionStateSnapshot {
readonly skillActive?: boolean;
readonly productName?: string;
readonly replyStyleGuide?: string;
readonly agentsMdStatus?: 'missing' | 'empty' | 'present';
[key: string]: unknown;
}) => string;
readonly renderSystemPrompt?: (context: /* AgentProfileContext — packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts */ {
@ -220,7 +218,6 @@ export interface SessionStateSnapshot {
readonly skillActive?: boolean;
readonly productName?: string;
readonly replyStyleGuide?: string;
readonly agentsMdStatus?: 'missing' | 'empty' | 'present';
[key: string]: unknown;
}) => /* SystemPromptRenderResult — packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts */ {
readonly text: string;
@ -235,15 +232,6 @@ export interface SessionStateSnapshot {
} | {
readonly disclosed: false;
};
readonly agentsMd: {
readonly disclosed: true;
readonly value: {
readonly fingerprint: string;
readonly status: /* AgentsMdStatus — packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts */ 'missing' | 'empty' | 'present';
};
} | {
readonly disclosed: false;
};
};
};
readonly promptPrefix?: (ctx: /* AgentProfilePromptPrefixContext — packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts */ {
@ -310,7 +298,6 @@ export interface SessionStateSnapshot {
readonly skillActive?: boolean;
readonly productName?: string;
readonly replyStyleGuide?: string;
readonly agentsMdStatus?: 'missing' | 'empty' | 'present';
[key: string]: unknown;
}) => string;
readonly renderSystemPrompt?: (context: /* AgentProfileContext — packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts */ {
@ -326,7 +313,6 @@ export interface SessionStateSnapshot {
readonly skillActive?: boolean;
readonly productName?: string;
readonly replyStyleGuide?: string;
readonly agentsMdStatus?: 'missing' | 'empty' | 'present';
[key: string]: unknown;
}) => /* SystemPromptRenderResult — packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts */ {
readonly text: string;
@ -341,15 +327,6 @@ export interface SessionStateSnapshot {
} | {
readonly disclosed: false;
};
readonly agentsMd: {
readonly disclosed: true;
readonly value: {
readonly fingerprint: string;
readonly status: /* AgentsMdStatus — packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts */ 'missing' | 'empty' | 'present';
};
} | {
readonly disclosed: false;
};
};
};
readonly promptPrefix?: (ctx: /* AgentProfilePromptPrefixContext — packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts */ {
@ -749,11 +726,6 @@ export interface SessionStateSnapshot {
readonly renderGeneration: number;
readonly localDate: string;
readonly timeZone: string;
} | {
readonly kind: 'agents_md';
readonly renderGeneration: number;
readonly fingerprint: string;
readonly status: /* AgentsMdStatus — packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts */ 'missing' | 'empty' | 'present';
} | {
readonly kind: 'skills';
readonly renderGeneration: number;
@ -840,11 +812,6 @@ export interface AgentStateSnapshot {
readonly renderGeneration: number;
readonly localDate: string;
readonly timeZone: string;
} | {
readonly kind: 'agents_md';
readonly renderGeneration: number;
readonly fingerprint: string;
readonly status: /* AgentsMdStatus — packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts */ 'missing' | 'empty' | 'present';
} | {
readonly kind: 'skills';
readonly renderGeneration: number;
@ -979,11 +946,6 @@ export interface AgentStateSnapshot {
readonly renderGeneration: number;
readonly localDate: string;
readonly timeZone: string;
} | {
readonly kind: 'agents_md';
readonly renderGeneration: number;
readonly fingerprint: string;
readonly status: /* AgentsMdStatus — packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts */ 'missing' | 'empty' | 'present';
} | {
readonly kind: 'skills';
readonly renderGeneration: number;
@ -1050,11 +1012,6 @@ export interface AgentStateSnapshot {
readonly renderGeneration: number;
readonly localDate: string;
readonly timeZone: string;
} | {
readonly kind: 'agents_md';
readonly renderGeneration: number;
readonly fingerprint: string;
readonly status: /* AgentsMdStatus — packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts */ 'missing' | 'empty' | 'present';
} | {
readonly kind: 'skills';
readonly renderGeneration: number;
@ -1162,7 +1119,7 @@ export interface AgentStateSnapshot {
'llmRequester.lastConfigLogSignature': string | undefined;
'llmRequester.mediaDegradedTurns': Set<number>;
'llmRequester.mediaStrippedTurns': Map<number, /* MediaStripSnapshot — packages/agent-core-v2/src/agent/contextProjector/contextProjector.ts */ {
readonly "__@mediaStripSnapshotBrand@2779": undefined;
readonly "__@mediaStripSnapshotBrand@2736": undefined;
}>;
'llmRequester.turnConfigs': Map<number, /* TurnRequestConfig — packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts */ {
readonly resolved: /* ProfileModelContext — packages/agent-core-v2/src/agent/profile/profile.ts */ {
@ -1238,13 +1195,6 @@ export interface AgentStateSnapshot {
'permissionMode.lastMode': 'manual' | 'yolo' | 'auto' | undefined;
// src/agent/plan/injection/planModeInjection.ts
'plan.wasActive': boolean;
// src/agent/profile/agentsMdReminderService.ts
'agentsMdReminder.seed': /* AgentsMdSeed — packages/agent-core-v2/src/agent/profile/agentsMdReminderService.ts */ {
readonly cwd: string;
readonly fingerprint: string;
readonly status: /* AgentsMdStatus — packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts */ 'missing' | 'empty' | 'present';
readonly renderGeneration: number;
} | undefined;
// src/agent/profile/profileService.ts
'profile.activeToolNamesOverlay': readonly string[] | undefined;
'profile.agentsMdWarning': string | undefined;

View file

@ -87,7 +87,6 @@ interface ConfigUpdatePayload {
environmentDisclosure?: {
cwd: string;
date: { disclosed: true, value: { localDate: string, timeZone: string } } | { disclosed: false };
agentsMd: { disclosed: true, value: { fingerprint: string, status: 'missing' | 'empty' | 'present' } } | { disclosed: false };
};
renderGeneration?: number;
disallowedTools?: string[];
@ -474,7 +473,6 @@ interface ProfileBindPayload {
environmentDisclosure?: {
cwd: string;
date: { disclosed: true, value: { localDate: string, timeZone: string } } | { disclosed: false };
agentsMd: { disclosed: true, value: { fingerprint: string, status: 'missing' | 'empty' | 'present' } } | { disclosed: false };
};
renderGeneration?: number;
activeToolNames?: string[];

View file

@ -1,6 +1,6 @@
/**
* `contextInjector` domain (L4) shared disclosure-baseline helpers for the
* reminder providers (`date_change`, `agents_md`, `skill_list`).
* reminder providers (`date_change`, `skill_list`).
*
* A provider's baseline answers "what has the model already seen" from up to
* three sources, compared by render generation with ties won by the earlier

View file

@ -1,7 +1,6 @@
import type { ContentPart, Message } from '#/kosong/contract/message';
import type { AgentTaskStatus } from '#/agent/task/task';
import type { AgentsMdStatus } from '#/app/agentProfileCatalog/agentProfileCatalog';
export type SkillSource = 'project' | 'user' | 'extra' | 'builtin';
@ -45,12 +44,6 @@ export type ContextInjectionDisclosure =
readonly localDate: string;
readonly timeZone: string;
}
| {
readonly kind: 'agents_md';
readonly renderGeneration: number;
readonly fingerprint: string;
readonly status: AgentsMdStatus;
}
| {
readonly kind: 'skills';
readonly renderGeneration: number;

View file

@ -1,17 +0,0 @@
/**
* `profile` domain (L4) `IAgentAgentsMdReminderService` contract.
*
* Defines the Agent-scope marker service that announces AGENTS.md content
* changes (edits, creations, removals) through an `agents_md`
* context-injection reminder when the files drift from what the system prompt
* was rendered with.
*/
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
export interface IAgentAgentsMdReminderService {
readonly _serviceBrand: undefined;
}
export const IAgentAgentsMdReminderService: ServiceIdentifier<IAgentAgentsMdReminderService> =
createDecorator<IAgentAgentsMdReminderService>('agentAgentsMdReminderService');

View file

@ -1,159 +0,0 @@
/**
* `profile` domain (L4) `IAgentAgentsMdReminderService` implementation.
*
* Owns the `agents_md` context-injection provider. The AGENTS.md instruction
* hierarchy is baked into the system prompt at (re)bind and after compaction,
* so a user editing AGENTS.md mid-session leaves the model following stale
* rules; this provider injects the fresh content at the next step boundary
* when it differs. Unlike skills, removals and content edits announce too
* a deleted rule fails silently, never on invocation. The provider runs only
* while the profile's rendered snapshot exists and matches the live cwd. The
* baseline prefers the typed disclosure on the newest surviving `agents_md`
* injection, then the persisted rendered snapshot, then a runtime seed kept
* in `agentState`: a profile whose snapshot declares no AGENTS.md disclosure
* is seeded with the first observed fingerprint (quietly), so a later
* creation, edit, or removal still announces. The live content is re-read at
* every step boundary the candidate chain is a handful of small instruction
* files, so no filesystem watch is kept. Bound at Agent scope.
*/
import { Disposable } from '#/_base/di/lifecycle';
import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { defineState } from '#/_base/state/stateRegistry';
import {
IAgentContextInjectorService,
type ContextInjectionContext,
type ContextInjectionResult,
} from '#/agent/contextInjector/contextInjector';
import {
disclosureOfKind,
pickDisclosureBaseline,
} from '#/agent/contextInjector/disclosureBaseline';
import { IAgentStateService } from '#/agent/state/agentState';
import type { AgentsMdStatus } from '#/app/agentProfileCatalog/agentProfileCatalog';
import { fingerprintDisclosureContent } from '#/app/agentProfileCatalog/profile-shared';
import { IBootstrapService } from '#/app/bootstrap/bootstrap';
import { IHostEnvironment } from '#/os/interface/hostEnvironment';
import { IHostFileSystem } from '#/os/interface/hostFileSystem';
import { loadAgentsMdSnapshot } from './context';
import { IAgentProfileService } from './profile';
import { IAgentAgentsMdReminderService } from './agentsMdReminder';
const AGENTS_MD_INJECTION_VARIANT = 'agents_md';
const CURRENT_BLOCK_START = '<current-agents-md>';
const CURRENT_BLOCK_END = '</current-agents-md>';
export const agentsMdReminderSeedKey = defineState<AgentsMdSeed | undefined>(
'agentsMdReminder.seed',
() => undefined,
);
export class AgentAgentsMdReminderService extends Disposable implements IAgentAgentsMdReminderService {
declare readonly _serviceBrand: undefined;
constructor(
@IAgentContextInjectorService dynamicInjector: IAgentContextInjectorService,
@IAgentProfileService private readonly profile: IAgentProfileService,
@IAgentStateService private readonly states: IAgentStateService,
@IHostFileSystem private readonly fs: IHostFileSystem,
@IHostEnvironment private readonly env: IHostEnvironment,
@IBootstrapService private readonly bootstrap: IBootstrapService,
) {
super();
this.states.register(agentsMdReminderSeedKey);
this._register(
dynamicInjector.register(AGENTS_MD_INJECTION_VARIANT, (ctx) => this.reminder(ctx)),
);
}
private async reminder({
lastDisclosure,
}: ContextInjectionContext): Promise<ContextInjectionResult | undefined> {
try {
let profileData = this.profile.data();
if (profileData.environmentDisclosure?.cwd !== profileData.cwd) return undefined;
const current = await loadAgentsMdSnapshot(
{ fs: this.fs, homeDir: this.env.homeDir },
profileData.cwd,
this.bootstrap.homeDir,
);
profileData = this.profile.data();
if (profileData.environmentDisclosure?.cwd !== profileData.cwd) return undefined;
const renderGeneration = profileData.renderGeneration ?? 0;
const fingerprint = fingerprintDisclosureContent(current.content);
const baseline = pickDisclosureBaseline<AgentsMdDisclosure>(
disclosureOfKind(lastDisclosure, 'agents_md'),
this.contentFromProfile(),
this.seed(),
);
if (baseline === undefined) {
this.states.set(agentsMdReminderSeedKey, {
fingerprint,
status: current.status,
renderGeneration,
cwd: profileData.cwd,
});
return undefined;
}
if (baseline.fingerprint === fingerprint) {
return undefined;
}
return {
content: buildAgentsMdReminder(current.content),
disclosure: {
kind: 'agents_md',
renderGeneration,
fingerprint,
status: current.status,
},
};
} catch {
return undefined;
}
}
private contentFromProfile(): AgentsMdDisclosure | undefined {
const profileData = this.profile.data();
if (profileData.environmentDisclosure?.cwd !== profileData.cwd) return undefined;
const agentsMd = profileData.environmentDisclosure.agentsMd;
if (!agentsMd?.disclosed) return undefined;
return {
...agentsMd.value,
renderGeneration: profileData.renderGeneration ?? 0,
};
}
private seed(): AgentsMdDisclosure | undefined {
const seed = this.states.get(agentsMdReminderSeedKey);
if (seed === undefined || seed.cwd !== this.profile.data().cwd) return undefined;
return seed;
}
}
interface AgentsMdDisclosure {
readonly fingerprint: string;
readonly status: AgentsMdStatus;
readonly renderGeneration: number;
}
interface AgentsMdSeed extends AgentsMdDisclosure {
readonly cwd: string;
}
function buildAgentsMdReminder(current: string): string {
const body =
current.length > 0
? 'The AGENTS.md instructions have changed since your system prompt was rendered. The content below is current and supersedes the AGENTS.md instructions in your system prompt.'
: 'The AGENTS.md instructions that fed your system prompt have been removed (or are now empty); they no longer apply.';
return `${body}\n\n${CURRENT_BLOCK_START}\n${current}\n${CURRENT_BLOCK_END}\n\nDO NOT mention this to the user explicitly.`;
}
registerScopedService(
LifecycleScope.Agent,
IAgentAgentsMdReminderService,
AgentAgentsMdReminderService,
ScopeActivation.OnScopeCreated,
'profile',
);

View file

@ -20,7 +20,6 @@ import { dirname, join, normalize } from 'pathe';
import type { IHostFileSystem } from '#/os/interface/hostFileSystem';
import type { AgentsMdStatus } from '#/app/agentProfileCatalog/agentProfileCatalog';
import type { SystemPromptContext } from './profile';
export const AGENTS_MD_RECOMMENDED_MAX_BYTES = 32 * 1024;
@ -38,7 +37,6 @@ export interface PreparedSystemPromptContext extends SystemPromptContext {
readonly agentsMd?: string;
readonly additionalDirsInfo?: string;
readonly agentsMdWarning?: string;
readonly agentsMdStatus?: AgentsMdStatus;
}
export interface PrepareSystemPromptContextOptions {
@ -60,7 +58,6 @@ export async function prepareSystemPromptContext(
return {
cwdListing,
agentsMd: agentsMdResult.content,
agentsMdStatus: agentsMdResult.status,
additionalDirsInfo,
agentsMdWarning: agentsMdResult.warning,
};
@ -71,17 +68,8 @@ export async function loadAgentsMd(
workDir: string,
brandHome?: string,
): Promise<string> {
const result = await loadAgentsMdSnapshot(deps, workDir, brandHome);
return result.content;
}
export async function loadAgentsMdSnapshot(
deps: ProfileContextDeps,
workDir: string,
brandHome?: string,
): Promise<{ readonly content: string; readonly status: AgentsMdStatus }> {
const result = await loadAgentsMdForRoots(deps, brandHome, [workDir]);
return { content: result.content, status: result.status };
return result.content;
}
async function agentsMdCandidateGroups(
@ -109,7 +97,6 @@ async function agentsMdCandidateGroups(
interface LoadedAgentsMd {
readonly content: string;
readonly warning: string | undefined;
readonly status: AgentsMdStatus;
}
async function loadAgentsMdForRoots(
@ -120,15 +107,12 @@ async function loadAgentsMdForRoots(
const discovered: AgentFile[] = [];
const seen = new Set<string>();
const loadWarnings: string[] = [];
let emptyFileFound = false;
const warnLoad = (message: string): void => {
loadWarnings.push(message);
};
const collect = async (path: string): Promise<boolean> => {
const file = await readAgentFile(deps, path, warnLoad, () => {
emptyFileFound = true;
});
const file = await readAgentFile(deps, path, warnLoad);
if (file === undefined) return false;
const key = normalize(file.path);
if (seen.has(key)) return false;
@ -154,11 +138,7 @@ async function loadAgentsMdForRoots(
);
}
const warning = loadWarnings.length > 0 ? loadWarnings.join('\n') : undefined;
return {
content,
warning,
status: content.length > 0 ? 'present' : emptyFileFound ? 'empty' : 'missing',
};
return { content, warning };
}
async function loadAdditionalDirsInfo(
@ -210,7 +190,6 @@ async function readAgentFile(
deps: ProfileContextDeps,
path: string,
warn: (message: string) => void,
markEmpty: () => void,
): Promise<AgentFile | undefined> {
if (!(await isFile(deps, path))) {
if (await entryExists(deps, path)) {
@ -226,7 +205,6 @@ async function readAgentFile(
return undefined;
}
if (content.length === 0) {
markEmpty();
return undefined;
}
return { path, content };

View file

@ -67,20 +67,14 @@ export interface AgentProfileContext {
readonly skillActive?: boolean;
readonly productName?: string;
readonly replyStyleGuide?: string;
readonly agentsMdStatus?: AgentsMdStatus;
readonly [key: string]: unknown;
}
export type AgentsMdStatus = 'missing' | 'empty' | 'present';
export interface EnvironmentDisclosureSnapshot {
readonly cwd: string;
readonly date:
| { readonly disclosed: true; readonly value: { readonly localDate: string; readonly timeZone: string } }
| { readonly disclosed: false };
readonly agentsMd:
| { readonly disclosed: true; readonly value: { readonly fingerprint: string; readonly status: AgentsMdStatus } }
| { readonly disclosed: false };
}
export interface SystemPromptRenderResult {
@ -124,7 +118,6 @@ export function renderAgentProfile(
environment: {
cwd: context.cwd ?? '',
date: { disclosed: false },
agentsMd: { disclosed: false },
},
}
);

View file

@ -28,8 +28,6 @@
* parse the rendered text.
*/
import { createHash } from 'node:crypto';
import { renderPrompt } from '#/_base/utils/render-prompt';
import {
@ -183,7 +181,6 @@ function environmentForTemplate(
context: AgentProfileContext,
): EnvironmentDisclosureSnapshot {
const usesNow = template.includes('${now}');
const usesAgentsMd = template.includes('${agents_md}');
return {
cwd: context.cwd ?? '',
date: usesNow
@ -195,15 +192,6 @@ function environmentForTemplate(
},
}
: { disclosed: false },
agentsMd: usesAgentsMd
? {
disclosed: true,
value: {
fingerprint: fingerprintDisclosureContent(context.agentsMd ?? ''),
status: context.agentsMdStatus ?? agentsMdStatus(context.agentsMd),
},
}
: { disclosed: false },
};
}
@ -211,7 +199,6 @@ function undisclosedEnvironment(context: AgentProfileContext): EnvironmentDisclo
return {
cwd: context.cwd ?? '',
date: { disclosed: false },
agentsMd: { disclosed: false },
};
}
@ -223,18 +210,9 @@ function mergeEnvironmentDisclosure(
return {
cwd: direct.cwd || base.cwd,
date: direct.date.disclosed ? direct.date : base.date,
agentsMd: direct.agentsMd.disclosed ? direct.agentsMd : base.agentsMd,
};
}
export function fingerprintDisclosureContent(content: string): string {
return createHash('sha256').update(content, 'utf8').digest('hex');
}
export function agentsMdStatus(content: string | undefined): 'missing' | 'empty' | 'present' {
return content === undefined ? 'missing' : content.length === 0 ? 'empty' : 'present';
}
function localDateKey(now: string | undefined): string {
const date = now === undefined ? new Date() : new Date(now);
if (Number.isNaN(date.getTime())) return localDateKey(undefined);

View file

@ -529,8 +529,6 @@ export * from '#/agent/permissionRules/permissionRulesService';
export * from '#/agent/profile/profile';
export * from '#/agent/profile/profileService';
export * from '#/agent/profile/context';
export * from '#/agent/profile/agentsMdReminder';
export * from '#/agent/profile/agentsMdReminderService';
export * from '#/agent/prompt/prompt';
export * from '#/agent/prompt/promptService';
import '#/app/messageLegacy/errors';

View file

@ -64,7 +64,6 @@ function updateSystemPromptWithDate(
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
},
},
agentsMd: { disclosed: false },
};
profile.update({
systemPrompt: systemPromptWithDate(iso),
@ -77,7 +76,6 @@ function updateSystemPromptWithoutDate(profile: IAgentProfileService): void {
const environment: EnvironmentDisclosureSnapshot = {
cwd: profile.data().cwd,
date: { disclosed: false },
agentsMd: { disclosed: false },
};
profile.update({
systemPrompt: 'You are a deterministic test agent.',

View file

@ -1,299 +0,0 @@
/**
* Scenario: `agents_md` context injection announces AGENTS.md content changes.
*
* Exercises the real provider through the harness injector against a fake
* host fs whose AGENTS.md files the test edits and deletes: the provider
* re-reads the candidate chain on every injection, so a filesystem edit
* lands on the next `inject()` call. Baselines come from typed reminder
* metadata, the persisted rendered snapshot, and a runtime seed recorded on
* first observation for prompts that never disclose AGENTS.md. Edits,
* creations, and removals all announce. Run: `pnpm --filter
* @moonshot-ai/agent-core-v2 exec vitest run
* test/agent/profile/agentsMdReminder.test.ts`.
*/
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { IAgentContextInjectorService } from '#/agent/contextInjector/contextInjector';
import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory';
import type { ContextMessage } from '#/agent/contextMemory/types';
import { loadAgentsMd } from '#/agent/profile/context';
import { IAgentProfileService } from '#/agent/profile/profile';
import {
fingerprintDisclosureContent,
agentsMdStatus,
} from '#/app/agentProfileCatalog/profile-shared';
import type { EnvironmentDisclosureSnapshot } from '#/app/agentProfileCatalog/agentProfileCatalog';
import type { HostFileStat, IHostFileSystem } from '#/os/interface/hostFileSystem';
import { createFakeHostFs } from '../../tools/fixtures/fake-exec';
import { createTestAgent, execEnvServices, type TestAgentContext } from '../../harness';
const TEST_HOME_DIR = '/home/test';
const BRAND_HOME_DIR = '/tmp/kimi-code-agent-app-v2-test';
function systemPromptWithAgentsMd(content: string): string {
return [
'You are a deterministic test agent.',
'',
'The applicable `AGENTS.md` instructions are:',
'',
'```````',
content,
'```````',
].join('\n');
}
function updateSystemPromptWithAgentsMd(
profile: IAgentProfileService,
content: string,
): void {
const environment: EnvironmentDisclosureSnapshot = {
cwd: profile.data().cwd,
date: { disclosed: false },
agentsMd: {
disclosed: true,
value: {
fingerprint: fingerprintDisclosureContent(content),
status: agentsMdStatus(content),
},
},
};
profile.update({
systemPrompt: systemPromptWithAgentsMd(content),
environmentDisclosure: environment,
});
}
function updateSystemPromptWithoutAgentsMd(profile: IAgentProfileService): void {
const environment: EnvironmentDisclosureSnapshot = {
cwd: profile.data().cwd,
date: { disclosed: false },
agentsMd: { disclosed: false },
};
profile.update({
systemPrompt: 'You are a deterministic test agent.',
environmentDisclosure: environment,
});
}
function agentsMdReminders(context: IAgentContextMemoryService): readonly ContextMessage[] {
return context.get().filter((message) => {
return message.origin?.kind === 'injection' && message.origin.variant === 'agents_md';
});
}
function messageText(message: ContextMessage): string {
return message.content
.map((part) => (part.type === 'text' ? part.text : ''))
.join('');
}
function deferred<T>(): {
readonly promise: Promise<T>;
readonly resolve: (value: T) => void;
} {
let resolve!: (value: T) => void;
const promise = new Promise<T>((done) => {
resolve = done;
});
return { promise, resolve };
}
describe('AgentAgentsMdReminderService', () => {
let cwd: string;
let agentsMdPath: string;
let files: Map<string, string>;
let dirs: Set<string>;
let hostFs: IHostFileSystem;
let ctx: TestAgentContext;
let context: IAgentContextMemoryService;
let injector: IAgentContextInjectorService;
let profile: IAgentProfileService;
let readTextOverride: ((path: string) => Promise<string>) | undefined;
function fileStat(path: string): HostFileStat {
return { isFile: true, isDirectory: false, size: files.get(path)?.length ?? 0 };
}
async function currentAgentsMd(): Promise<string> {
return loadAgentsMd({ fs: hostFs, homeDir: TEST_HOME_DIR }, cwd, BRAND_HOME_DIR);
}
beforeEach(() => {
cwd = process.cwd();
agentsMdPath = `${cwd}/AGENTS.md`;
files = new Map();
dirs = new Set([cwd, `${cwd}/.git`]);
readTextOverride = undefined;
hostFs = createFakeHostFs({
stat: async (path: string) => {
if (files.has(path)) return fileStat(path);
if (dirs.has(path)) return { isFile: false, isDirectory: true, size: 0 };
throw new Error(`ENOENT: ${path}`);
},
lstat: async (path: string) => {
if (files.has(path)) return fileStat(path);
if (dirs.has(path)) return { isFile: false, isDirectory: true, size: 0 };
throw new Error(`ENOENT: ${path}`);
},
readText: async (path: string) => {
if (readTextOverride !== undefined) return readTextOverride(path);
const content = files.get(path);
if (content === undefined) throw new Error(`ENOENT: ${path}`);
return content;
},
readdir: async () => [],
realpath: async (path: string) => path,
});
ctx = createTestAgent(execEnvServices({ hostFs }));
context = ctx.get(IAgentContextMemoryService);
injector = ctx.get(IAgentContextInjectorService);
profile = ctx.get(IAgentProfileService);
});
afterEach(async () => {
try {
await ctx.expectResumeMatches();
} finally {
await ctx.dispose();
}
});
it('stays quiet when no AGENTS.md exists and the prompt has no fenced block', async () => {
updateSystemPromptWithoutAgentsMd(profile);
await injector.inject();
expect(agentsMdReminders(context)).toHaveLength(0);
expect(context.get()).toHaveLength(0);
});
it('announces a file created after the silent seed', async () => {
updateSystemPromptWithoutAgentsMd(profile);
await injector.inject();
expect(agentsMdReminders(context)).toHaveLength(0);
files.set(agentsMdPath, 'fresh rule');
await injector.inject();
const reminders = agentsMdReminders(context);
expect(reminders).toHaveLength(1);
expect(messageText(reminders[0] as ContextMessage)).toContain('fresh rule');
await injector.inject();
expect(agentsMdReminders(context)).toHaveLength(1);
});
it('stays quiet when the file content matches the system prompt block', async () => {
files.set(agentsMdPath, 'rule one');
updateSystemPromptWithAgentsMd(profile, await currentAgentsMd());
await injector.inject();
expect(agentsMdReminders(context)).toHaveLength(0);
});
it('injects the fresh content after an edit, then stays quiet', async () => {
files.set(agentsMdPath, 'rule one');
updateSystemPromptWithAgentsMd(profile, await currentAgentsMd());
await injector.inject();
files.set(agentsMdPath, 'rule two');
await injector.inject();
const reminders = agentsMdReminders(context);
expect(reminders).toHaveLength(1);
const first = reminders[0];
expect(first).toBeDefined();
const text = messageText(first as ContextMessage);
expect(text).toContain('rule two');
expect(text).toContain('supersedes');
expect(text).toContain('DO NOT mention this to the user explicitly');
expect(first?.origin).toMatchObject({
kind: 'injection',
variant: 'agents_md',
disclosure: {
kind: 'agents_md',
fingerprint: fingerprintDisclosureContent(await currentAgentsMd()),
status: 'present',
},
});
await injector.inject();
expect(agentsMdReminders(context)).toHaveLength(1);
});
it('announces an edit that lands while fresh content is being read', async () => {
files.set(agentsMdPath, 'rule one');
updateSystemPromptWithAgentsMd(profile, await currentAgentsMd());
await injector.inject();
files.set(agentsMdPath, 'rule two');
const readStarted = deferred<void>();
const releaseRead = deferred<void>();
let pauseRead = true;
readTextOverride = async (path) => {
const content = files.get(path);
if (content === undefined) throw new Error(`ENOENT: ${path}`);
if (path === agentsMdPath && pauseRead) {
pauseRead = false;
readStarted.resolve(undefined);
await releaseRead.promise;
}
return content;
};
const firstInjection = injector.inject();
await readStarted.promise;
files.set(agentsMdPath, 'rule three');
releaseRead.resolve(undefined);
await firstInjection;
await injector.inject();
const reminders = agentsMdReminders(context);
expect(reminders).toHaveLength(2);
expect(messageText(reminders[0] as ContextMessage)).toContain('rule two');
expect(messageText(reminders[1] as ContextMessage)).toContain('rule three');
});
it('announces removal when the last AGENTS.md file disappears', async () => {
files.set(agentsMdPath, 'rule one');
updateSystemPromptWithAgentsMd(profile, await currentAgentsMd());
await injector.inject();
files.clear();
await injector.inject();
const reminders = agentsMdReminders(context);
expect(reminders).toHaveLength(1);
const first = reminders[0];
expect(first).toBeDefined();
expect(messageText(first as ContextMessage)).toContain('removed');
expect(first?.origin).toMatchObject({
disclosure: {
kind: 'agents_md',
fingerprint:
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
status: 'missing',
},
});
});
it('announces a file created after an empty baseline', async () => {
updateSystemPromptWithAgentsMd(profile, '');
await injector.inject();
files.set(agentsMdPath, 'fresh rule');
await injector.inject();
const reminders = agentsMdReminders(context);
expect(reminders).toHaveLength(1);
const first = reminders[0];
expect(first).toBeDefined();
expect(messageText(first as ContextMessage)).toContain('fresh rule');
});
});

View file

@ -357,14 +357,6 @@ describe('AgentProfileService (wire-backed config.update)', () => {
disclosed: true,
value: { localDate: '2026-07-29', timeZone: 'Asia/Shanghai' },
},
agentsMd: {
disclosed: true,
value: {
fingerprint:
'a3a18567b84af6528d171050e54bcf75e0a1c94c8db8a165d03e9866cfa95bce',
status: 'present',
},
},
};
svc.applyBindingSnapshot({
cwd: '/work',

View file

@ -141,14 +141,13 @@ describe('renderPromptTemplate', () => {
);
});
it('records the environment facts used by now and agents_md placeholders', () => {
it('records the environment facts used by the now placeholder', () => {
const result = renderPromptTemplateResult(
'date=${now} agents=${agents_md}',
{
cwd: '/work',
now: '2026-07-29T12:00:00',
agentsMd: 'AGENTS',
agentsMdStatus: 'present',
},
{ skillActive: true },
);
@ -159,13 +158,6 @@ describe('renderPromptTemplate', () => {
disclosed: true,
value: { localDate: '2026-07-29' },
});
expect(result.environment.agentsMd).toEqual({
disclosed: true,
value: {
fingerprint: '370f346f47cb65d94fd904ecb4ceeb17d784965984b0f8e52cce79ff95451a54',
status: 'present',
},
});
});
it('merges disclosure metadata from a structured base_prompt render', () => {
@ -181,14 +173,6 @@ describe('renderPromptTemplate', () => {
disclosed: true,
value: { localDate: '2026-07-28', timeZone: 'UTC' },
},
agentsMd: {
disclosed: true,
value: {
fingerprint:
'a3a18567b84af6528d171050e54bcf75e0a1c94c8db8a165d03e9866cfa95bce',
status: 'present',
},
},
},
}),
);
@ -200,14 +184,6 @@ describe('renderPromptTemplate', () => {
disclosed: true,
value: { localDate: '2026-07-28', timeZone: 'UTC' },
},
agentsMd: {
disclosed: true,
value: {
fingerprint:
'a3a18567b84af6528d171050e54bcf75e0a1c94c8db8a165d03e9866cfa95bce',
status: 'present',
},
},
});
});
});
@ -289,14 +265,13 @@ describe('renderSystemPrompt', () => {
expect(overridden).not.toContain('Kimi Code CLI');
});
it('returns disclosure metadata for the builtin now and agents_md sections', () => {
it('returns disclosure metadata for the builtin now section', () => {
const result = renderSystemPromptResult(
'',
{
cwd: '/work',
now: '2026-07-29T12:00:00',
agentsMd: 'AGENTS',
agentsMdStatus: 'present',
},
{ skillActive: true },
);
@ -307,12 +282,5 @@ describe('renderSystemPrompt', () => {
disclosed: true,
value: { localDate: '2026-07-29' },
});
expect(result.environment.agentsMd).toEqual({
disclosed: true,
value: {
fingerprint: '370f346f47cb65d94fd904ecb4ceeb17d784965984b0f8e52cce79ff95451a54',
status: 'present',
},
});
});
});

View file

@ -195,7 +195,7 @@ describe('Agent config', () => {
});
expect(ctx.newEvents()).toMatchInlineSnapshot(`
[wire] config.update { "profileName": "test-profile", "systemPrompt": "Profile system prompt.", "environmentDisclosure": { "cwd": "<cwd>", "date": { "disclosed": false }, "agentsMd": { "disclosed": false } }, "renderGeneration": 2, "disallowedTools": [], "time": "<time>" }
[wire] config.update { "profileName": "test-profile", "systemPrompt": "Profile system prompt.", "environmentDisclosure": { "cwd": "<cwd>", "date": { "disclosed": false } }, "renderGeneration": 2, "disallowedTools": [], "time": "<time>" }
[emit] agent.status.updated { "model": "mock-model", "maxContextTokens": 1000000 }
[wire] tools.set_active_tools { "names": [ "Read" ], "time": "<time>" }
`);