mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
fix(agent-core-v2): refresh system prompt after compaction
This commit is contained in:
parent
14626234a4
commit
b159946fc9
6 changed files with 184 additions and 30 deletions
|
|
@ -307,6 +307,7 @@ fullCompaction --> llmRequester #34495E
|
|||
fullCompaction --> profile #34495E
|
||||
fullCompaction --> toolRegistry #34495E
|
||||
fullCompaction --> telemetry #34495E
|
||||
fullCompaction --> log #34495E
|
||||
fullCompaction --> wireRecord #34495E
|
||||
fullCompaction --> turn #34495E
|
||||
fullCompaction --> loop #34495E
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Disposable } from "#/_base/di/lifecycle";
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { ILogService } from '#/_base/log/log';
|
||||
import { renderPrompt } from "#/_base/utils/render-prompt";
|
||||
import {
|
||||
estimateTokens,
|
||||
|
|
@ -132,6 +133,7 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
|
|||
@IAgentWireService private readonly wire: IWireService,
|
||||
@IEventBus private readonly eventBus: IEventBus,
|
||||
@IAgentTurnService private readonly turn: IAgentTurnService,
|
||||
@ILogService private readonly log: ILogService,
|
||||
@IAgentLoopService loopService: IAgentLoopService,
|
||||
) {
|
||||
super();
|
||||
|
|
@ -421,6 +423,11 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
|
|||
try {
|
||||
const result = await this.compactionRound(active, data);
|
||||
if (this._compacting !== active) throw compactionCancelledReason(active);
|
||||
try {
|
||||
await this.profile.refreshSystemPrompt();
|
||||
} catch (error) {
|
||||
this.log.error('failed to refresh system prompt after compaction', { error });
|
||||
}
|
||||
this.lastCompactedTokenCount = result.tokensAfter;
|
||||
if (!this.markCompleted(active)) {
|
||||
throw compactionCancelledReason(active);
|
||||
|
|
|
|||
|
|
@ -153,6 +153,11 @@ export interface IAgentProfileService {
|
|||
* {@link getAgentsMdWarning} / `getSessionWarnings`.
|
||||
*/
|
||||
applyProfile(profile: ResolvedAgentProfile, options?: ApplyProfileOptions): Promise<void>;
|
||||
/**
|
||||
* Re-render the active profile's system prompt from freshly gathered runtime
|
||||
* context without changing the active tool set.
|
||||
*/
|
||||
refreshSystemPrompt(): Promise<void>;
|
||||
/**
|
||||
* The AGENTS.md size warning produced by the most recent {@link applyProfile},
|
||||
* if the combined AGENTS.md content exceeded the recommended soft budget.
|
||||
|
|
|
|||
|
|
@ -110,6 +110,8 @@ export class AgentProfileService implements IAgentProfileService {
|
|||
);
|
||||
}
|
||||
|
||||
private activeProfile: ResolvedAgentProfile | undefined;
|
||||
|
||||
constructor(
|
||||
@IAgentWireService private readonly wire: IWireService,
|
||||
@IEventBus private readonly eventBus: IEventBus,
|
||||
|
|
@ -137,6 +139,12 @@ export class AgentProfileService implements IAgentProfileService {
|
|||
|
||||
update(changed: ProfileUpdateData): void {
|
||||
const { activeToolNames, ...configChanged } = changed;
|
||||
if (
|
||||
changed.profileName !== undefined &&
|
||||
this.activeProfile?.name !== changed.profileName
|
||||
) {
|
||||
this.activeProfile = undefined;
|
||||
}
|
||||
if (Object.keys(configChanged).length > 0) {
|
||||
this.wire.dispatch(configUpdate(this.resolveConfigPayload(configChanged)));
|
||||
this.afterConfigDispatch(configChanged);
|
||||
|
|
@ -157,8 +165,8 @@ export class AgentProfileService implements IAgentProfileService {
|
|||
|
||||
const context = await this.buildSystemPromptContext(input.cwd);
|
||||
const systemPrompt = profile.systemPrompt(context);
|
||||
const { agentsMdWarning } = context;
|
||||
this.agentsMdWarning = agentsMdWarning;
|
||||
this.activeProfile = profile;
|
||||
this.cacheAgentsMdWarning(context);
|
||||
|
||||
const thinkingLevel = resolveThinkingEffort(
|
||||
input.thinking,
|
||||
|
|
@ -175,13 +183,7 @@ export class AgentProfileService implements IAgentProfileService {
|
|||
this.wire.dispatch(configUpdate({ modelAlias: input.model, thinkingEffort: thinkingLevel }));
|
||||
this.afterConfigDispatch({ modelAlias: input.model, thinkingLevel });
|
||||
|
||||
if (agentsMdWarning !== undefined) {
|
||||
this.eventBus.publish({
|
||||
type: 'warning',
|
||||
message: agentsMdWarning,
|
||||
code: 'agents-md-oversized',
|
||||
});
|
||||
}
|
||||
this.publishAgentsMdWarning();
|
||||
}
|
||||
|
||||
async setModel(alias: string): Promise<ProfileSetModelResult> {
|
||||
|
|
@ -213,6 +215,7 @@ export class AgentProfileService implements IAgentProfileService {
|
|||
}
|
||||
|
||||
useProfile(profile: ResolvedAgentProfile, context: SystemPromptContext): void {
|
||||
this.activeProfile = profile;
|
||||
this.update({
|
||||
profileName: profile.name,
|
||||
systemPrompt: profile.systemPrompt(context),
|
||||
|
|
@ -221,24 +224,24 @@ export class AgentProfileService implements IAgentProfileService {
|
|||
}
|
||||
|
||||
async applyProfile(profile: ResolvedAgentProfile, options?: ApplyProfileOptions): Promise<void> {
|
||||
const context = await prepareSystemPromptContext(
|
||||
{ fs: this.fs, homeDir: this.env.homeDir },
|
||||
this.sessionContext.cwd,
|
||||
this.bootstrap.homeDir,
|
||||
{
|
||||
additionalDirs: options?.additionalDirs ?? this.workspace.additionalDirs,
|
||||
},
|
||||
);
|
||||
const context = await this.buildSystemPromptContext(undefined, options);
|
||||
this.useProfile(profile, context);
|
||||
const { agentsMdWarning } = context;
|
||||
this.agentsMdWarning = agentsMdWarning;
|
||||
if (agentsMdWarning !== undefined) {
|
||||
this.eventBus.publish({
|
||||
type: 'warning',
|
||||
message: agentsMdWarning,
|
||||
code: 'agents-md-oversized',
|
||||
});
|
||||
}
|
||||
this.cacheAgentsMdWarning(context);
|
||||
this.publishAgentsMdWarning();
|
||||
}
|
||||
|
||||
async refreshSystemPrompt(): Promise<void> {
|
||||
const profile = this.resolveActiveProfile();
|
||||
if (profile === undefined) return;
|
||||
|
||||
const context = await this.buildSystemPromptContext(this.cwd);
|
||||
this.activeProfile = profile;
|
||||
this.update({
|
||||
profileName: profile.name,
|
||||
systemPrompt: profile.systemPrompt(context),
|
||||
});
|
||||
this.cacheAgentsMdWarning(context);
|
||||
this.publishAgentsMdWarning();
|
||||
}
|
||||
|
||||
getAgentsMdWarning(): string | undefined {
|
||||
|
|
@ -490,13 +493,37 @@ export class AgentProfileService implements IAgentProfileService {
|
|||
}
|
||||
}
|
||||
|
||||
private async buildSystemPromptContext(cwd?: string): Promise<SystemPromptContext> {
|
||||
private resolveActiveProfile(): ResolvedAgentProfile | undefined {
|
||||
if (this.activeProfile !== undefined) return this.activeProfile;
|
||||
const profileName = this.profileName;
|
||||
if (profileName === undefined) return undefined;
|
||||
return this.catalog.get(profileName);
|
||||
}
|
||||
|
||||
private cacheAgentsMdWarning(context: Pick<SystemPromptContext, 'agentsMdWarning'>): void {
|
||||
this.agentsMdWarning = context.agentsMdWarning;
|
||||
}
|
||||
|
||||
private publishAgentsMdWarning(): void {
|
||||
const warning = this.agentsMdWarning;
|
||||
if (warning === undefined) return;
|
||||
this.eventBus.publish({
|
||||
type: 'warning',
|
||||
message: warning,
|
||||
code: 'agents-md-oversized',
|
||||
});
|
||||
}
|
||||
|
||||
private async buildSystemPromptContext(
|
||||
cwd?: string,
|
||||
options?: ApplyProfileOptions,
|
||||
): Promise<SystemPromptContext> {
|
||||
const effectiveCwd = cwd ?? this.sessionContext.cwd;
|
||||
const base = await prepareSystemPromptContext(
|
||||
{ fs: this.fs, homeDir: this.env.homeDir },
|
||||
effectiveCwd,
|
||||
this.bootstrap.homeDir,
|
||||
{ additionalDirs: this.workspace.additionalDirs },
|
||||
{ additionalDirs: options?.additionalDirs ?? this.workspace.additionalDirs },
|
||||
);
|
||||
const skills = await this.resolveSkillListing();
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'pathe';
|
||||
|
||||
|
|
@ -19,14 +19,16 @@ import { MASTER_ENV } from '#/app/flag/flagService';
|
|||
import { estimateTokensForMessages } from '#/_base/utils/tokens';
|
||||
import { recordingTelemetry, type TelemetryRecord } from '../telemetry/stubs';
|
||||
import type { TestAgentContext, TestAgentOptions, TestAgentServiceOverride } from '../harness';
|
||||
import { appServices, createCommandRunner, execEnvServices, sessionServices, testAgent } from '../harness';
|
||||
import { appServices, createCommandRunner, execEnvServices, hostEnvironmentServices, sessionServices, testAgent } from '../harness';
|
||||
import {
|
||||
IAgentFullCompactionService,
|
||||
IOAuthService,
|
||||
IAgentProfileService,
|
||||
ISessionTodoService,
|
||||
type ResolvedAgentProfile,
|
||||
} from '#/index';
|
||||
import { IAgentTurnService } from '#/agent/turn/turn';
|
||||
import { HostFileSystem } from '#/os/backends/node-local/hostFsService';
|
||||
|
||||
type GenerateFn = NonNullable<TestAgentOptions['generate']>;
|
||||
|
||||
|
|
@ -53,6 +55,19 @@ const SNAPSHOT_VISIBLE_TOOLS = [
|
|||
'EnterPlanMode',
|
||||
'ExitPlanMode',
|
||||
] as const;
|
||||
const EXACT_COMPACTION_REFRESH_PROFILE: ResolvedAgentProfile = {
|
||||
name: 'exact-compaction-refresh',
|
||||
systemPrompt: (context) =>
|
||||
[
|
||||
`cwd:${context.cwd ?? ''}`,
|
||||
`os:${context.osKind ?? ''}`,
|
||||
`shell:${context.shellName ?? ''}:${context.shellPath ?? ''}`,
|
||||
`agents:${context.agentsMd ?? ''}`,
|
||||
`ls:${context.cwdListing ?? ''}`,
|
||||
`extra:${context.additionalDirsInfo ?? ''}`,
|
||||
].join('\n'),
|
||||
tools: ['Read', 'Write', 'Skill'],
|
||||
};
|
||||
|
||||
describe('FullCompaction', () => {
|
||||
it('keeps an oversized trailing user message as recent', () => {
|
||||
|
|
@ -278,6 +293,46 @@ describe('FullCompaction', () => {
|
|||
await ctx.expectResumeMatches();
|
||||
});
|
||||
|
||||
it('refreshes the active profile system prompt after compaction without resetting active tools', async () => {
|
||||
const homeDir = mkdtempSync(join(tmpdir(), 'kimi-compact-refresh-home-'));
|
||||
const workDir = mkdtempSync(join(tmpdir(), 'kimi-compact-refresh-work-'));
|
||||
try {
|
||||
writeFileSync(join(workDir, 'AGENTS.md'), 'old project instructions', 'utf-8');
|
||||
const ctx = testAgent(
|
||||
execEnvServices({ hostFs: new HostFileSystem() }),
|
||||
hostEnvironmentServices(homeDir),
|
||||
{ autoConfigure: false, cwd: workDir },
|
||||
);
|
||||
ctx.configureRuntimeModel(CATALOGUED_PROVIDER, CATALOGUED_MODEL_CAPABILITIES);
|
||||
const profile = ctx.get(IAgentProfileService);
|
||||
await profile.applyProfile(EXACT_COMPACTION_REFRESH_PROFILE);
|
||||
profile.update({ activeToolNames: ['Read'] });
|
||||
|
||||
expect(profile.data().systemPrompt).toBe(
|
||||
exactCompactionRefreshPrompt(workDir, 'old project instructions'),
|
||||
);
|
||||
|
||||
const refreshSpy = vi.spyOn(profile, 'refreshSystemPrompt');
|
||||
writeFileSync(join(workDir, 'AGENTS.md'), 'new project instructions', 'utf-8');
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendExchange(2, 'recent user two', 'recent assistant two', 80);
|
||||
const completed = ctx.once('compaction.completed');
|
||||
|
||||
ctx.mockNextResponse({ type: 'text', text: 'Compacted summary.' });
|
||||
await ctx.rpc.beginCompaction({});
|
||||
await completed;
|
||||
|
||||
expect(refreshSpy).toHaveBeenCalledTimes(1);
|
||||
expect(profile.data().systemPrompt).toBe(
|
||||
exactCompactionRefreshPrompt(workDir, 'new project instructions'),
|
||||
);
|
||||
expect(profile.getActiveToolNames()).toEqual(['Read']);
|
||||
} finally {
|
||||
rmSync(homeDir, { recursive: true, force: true });
|
||||
rmSync(workDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects a manual compaction while a turn is active', async () => {
|
||||
const ctx = testAgent(execEnvServices({ processRunner: createCommandRunner('should-not-run') }));
|
||||
ctx.configure({
|
||||
|
|
@ -2441,6 +2496,17 @@ function countEvents(events: ReturnType<TestAgentContext['newEvents']>, type: st
|
|||
}).length;
|
||||
}
|
||||
|
||||
function exactCompactionRefreshPrompt(workDir: string, agentsMd: string): string {
|
||||
return [
|
||||
`cwd:${workDir}`,
|
||||
'os:Linux',
|
||||
'shell:bash:/bin/bash',
|
||||
`agents:<!-- From: ${join(workDir, 'AGENTS.md')} -->\n${agentsMd}`,
|
||||
'ls:\u2514\u2500\u2500 AGENTS.md',
|
||||
'extra:',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function oauthTestAgentOptions(
|
||||
getAccessToken: (options?: { readonly force?: boolean }) => Promise<string>,
|
||||
): {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,20 @@ const profile: ResolvedAgentProfile = {
|
|||
tools: [],
|
||||
};
|
||||
|
||||
const exactProfile: ResolvedAgentProfile = {
|
||||
name: 'exact-profile',
|
||||
systemPrompt: (context) =>
|
||||
[
|
||||
`cwd:${context.cwd ?? ''}`,
|
||||
`os:${context.osKind ?? ''}`,
|
||||
`shell:${context.shellName ?? ''}:${context.shellPath ?? ''}`,
|
||||
`agents:${context.agentsMd ?? ''}`,
|
||||
`ls:${context.cwdListing ?? ''}`,
|
||||
`extra:${context.additionalDirsInfo ?? ''}`,
|
||||
].join('\n'),
|
||||
tools: ['Read', 'Write'],
|
||||
};
|
||||
|
||||
describe('AgentProfileService.applyProfile', () => {
|
||||
let ctx: TestAgentContext;
|
||||
let homeDir: string;
|
||||
|
|
@ -56,6 +70,29 @@ describe('AgentProfileService.applyProfile', () => {
|
|||
expect(svc.getAgentsMdWarning()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('renders the complete runtime context exactly', async () => {
|
||||
await writeFile(join(workDir, 'AGENTS.md'), 'project instructions', 'utf-8');
|
||||
const { profile: svc } = buildContext();
|
||||
|
||||
await svc.applyProfile(exactProfile);
|
||||
|
||||
expect(svc.data().systemPrompt).toBe(exactSystemPrompt(workDir, 'project instructions'));
|
||||
});
|
||||
|
||||
it('refreshes the active profile system prompt exactly without resetting active tools', async () => {
|
||||
await writeFile(join(workDir, 'AGENTS.md'), 'old instructions', 'utf-8');
|
||||
const { profile: svc } = buildContext();
|
||||
svc.update({ cwd: workDir });
|
||||
await svc.applyProfile(exactProfile);
|
||||
svc.update({ activeToolNames: ['Read'] });
|
||||
await writeFile(join(workDir, 'AGENTS.md'), 'new instructions', 'utf-8');
|
||||
|
||||
await svc.refreshSystemPrompt();
|
||||
|
||||
expect(svc.data().systemPrompt).toBe(exactSystemPrompt(workDir, 'new instructions'));
|
||||
expect(svc.getActiveToolNames()).toEqual(['Read']);
|
||||
});
|
||||
|
||||
it('caches an agents-md warning when the content exceeds the 32 KB soft budget', async () => {
|
||||
const largeContent = 'x'.repeat(40 * 1024);
|
||||
await writeFile(join(workDir, 'AGENTS.md'), largeContent, 'utf-8');
|
||||
|
|
@ -88,3 +125,14 @@ describe('AgentProfileService.applyProfile', () => {
|
|||
expect(svc.getAgentsMdWarning()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
function exactSystemPrompt(workDir: string, agentsMd: string): string {
|
||||
return [
|
||||
`cwd:${workDir}`,
|
||||
'os:Linux',
|
||||
'shell:bash:/bin/bash',
|
||||
`agents:<!-- From: ${join(workDir, 'AGENTS.md')} -->\n${agentsMd}`,
|
||||
'ls:\u2514\u2500\u2500 AGENTS.md',
|
||||
'extra:',
|
||||
].join('\n');
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue