mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
feat: replace silent AGENTS.md truncation with a visible warning (#1040)
Oversized AGENTS.md files are no longer silently truncated. The full content is injected, and a warning is shown in the TUI status bar and the web UI when the combined AGENTS.md size exceeds the recommended 32 KB. A generic session-warnings API backs this so future warning types can be added without changing the API surface.
This commit is contained in:
parent
0bcd9843c1
commit
66640380eb
20 changed files with 272 additions and 83 deletions
5
.changeset/soft-agents-md-warning.md
Normal file
5
.changeset/soft-agents-md-warning.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": minor
|
||||
---
|
||||
|
||||
Replace silent AGENTS.md truncation with a visible warning in the TUI status bar and web UI.
|
||||
|
|
@ -584,6 +584,7 @@ export class KimiTUI {
|
|||
}
|
||||
if (this.session !== undefined) {
|
||||
this.sessionEventHandler.startSubscription();
|
||||
void this.showSessionWarnings(this.session);
|
||||
}
|
||||
void this.fetchSessions();
|
||||
if (this.session !== undefined) {
|
||||
|
|
@ -592,6 +593,19 @@ export class KimiTUI {
|
|||
void this.refreshSkillCommands(this.session);
|
||||
}
|
||||
|
||||
private async showSessionWarnings(session: Session): Promise<void> {
|
||||
try {
|
||||
const warnings = await session.getSessionWarnings();
|
||||
if (this.session !== session) return;
|
||||
for (const warning of warnings) {
|
||||
const severity = warning.severity === 'error' ? 'error' : 'warning';
|
||||
this.showStatus(`Warning: ${warning.message}`, severity);
|
||||
}
|
||||
} catch {
|
||||
// Best-effort: startup must not block on warning retrieval.
|
||||
}
|
||||
}
|
||||
|
||||
private async showTmuxKeyboardWarningIfNeeded(): Promise<void> {
|
||||
const warning = await detectTmuxKeyboardWarning();
|
||||
if (warning === undefined || this.aborted) return;
|
||||
|
|
@ -1382,6 +1396,7 @@ export class KimiTUI {
|
|||
this.showStatus(`Warning: ${resumeState.warning}`, 'warning');
|
||||
}
|
||||
this.showStatus(statusMessage);
|
||||
void this.showSessionWarnings(session);
|
||||
}
|
||||
|
||||
async reloadCurrentSessionView(session: Session, statusMessage: string): Promise<void> {
|
||||
|
|
@ -1410,6 +1425,7 @@ export class KimiTUI {
|
|||
this.showStatus(`Warning: ${resumeState.warning}`, 'warning');
|
||||
}
|
||||
this.showStatus(statusMessage);
|
||||
void this.showSessionWarnings(session);
|
||||
}
|
||||
|
||||
async createNewSession(): Promise<void> {
|
||||
|
|
@ -1447,6 +1463,7 @@ export class KimiTUI {
|
|||
this.sessionEventHandler.startSubscription();
|
||||
this.clearTranscriptAndRedraw();
|
||||
this.showStatus(`Started a new session (${session.id}).`);
|
||||
void this.showSessionWarnings(session);
|
||||
void this.showConfigWarningsIfAny();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@ import type {
|
|||
WireProviderRefreshResult,
|
||||
WireSession,
|
||||
WireSessionAbortResult,
|
||||
WireSessionWarning,
|
||||
WireSessionWarningsResponse,
|
||||
WireSessionRuntimeStatus,
|
||||
WireSessionSnapshot,
|
||||
WireWorkspace,
|
||||
|
|
@ -394,6 +396,13 @@ export class DaemonKimiWebApi implements KimiWebApi {
|
|||
};
|
||||
}
|
||||
|
||||
async getSessionWarnings(sessionId: string): Promise<WireSessionWarning[]> {
|
||||
const data = await this.http.get<WireSessionWarningsResponse>(
|
||||
`/sessions/${encodeURIComponent(sessionId)}/warnings`,
|
||||
);
|
||||
return data.warnings ?? [];
|
||||
}
|
||||
|
||||
async archiveSession(sessionId: string): Promise<{ archived: true }> {
|
||||
const data = await this.http.post<WireArchiveResult>(
|
||||
`/sessions/${encodeURIComponent(sessionId)}:archive`,
|
||||
|
|
|
|||
|
|
@ -110,6 +110,17 @@ export interface WireSessionRuntimeStatus {
|
|||
context_usage: number;
|
||||
}
|
||||
|
||||
// GET /sessions/{id}/warnings — session-level warnings (e.g. oversized AGENTS.md).
|
||||
export interface WireSessionWarning {
|
||||
code: string;
|
||||
message: string;
|
||||
severity: 'info' | 'warning' | 'error';
|
||||
}
|
||||
|
||||
export interface WireSessionWarningsResponse {
|
||||
warnings: WireSessionWarning[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workspace + daemon folder browser wire DTOs
|
||||
// PRESUMED — not in the live daemon yet; isolated here, swap when backend ships.
|
||||
|
|
|
|||
|
|
@ -598,6 +598,12 @@ export interface AppSkill {
|
|||
// KimiWebApi — the app-facing interface
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface AppSessionWarning {
|
||||
code: string;
|
||||
message: string;
|
||||
severity: 'info' | 'warning' | 'error';
|
||||
}
|
||||
|
||||
export interface KimiWebApi {
|
||||
getHealth(): Promise<{ status: 'ok'; uptimeSec: number }>;
|
||||
getMeta(): Promise<{ serverVersion: string; serverId: string; startedAt: string; capabilities: Record<string, boolean>; openInApps: string[] }>;
|
||||
|
|
@ -607,6 +613,7 @@ export interface KimiWebApi {
|
|||
getSession(sessionId: string): Promise<AppSession>;
|
||||
updateSession(sessionId: string, input: { title?: string; cwd?: string; model?: string; permissionMode?: string; planMode?: boolean; swarmMode?: boolean; goalObjective?: string; goalControl?: 'pause' | 'resume' | 'cancel'; thinking?: string }): Promise<AppSession>;
|
||||
getSessionStatus(sessionId: string): Promise<AppSessionRuntimeStatus>;
|
||||
getSessionWarnings(sessionId: string): Promise<AppSessionWarning[]>;
|
||||
archiveSession(sessionId: string): Promise<{ archived: true }>;
|
||||
listMessages(sessionId: string, input?: PageRequest & { role?: AppMessageRole }): Promise<Page<AppMessage>>;
|
||||
/** v2 initial sync: atomic session state + `asOfSeq` watermark + epoch. */
|
||||
|
|
|
|||
|
|
@ -939,6 +939,22 @@ async function handleSessionNotFound(sessionId: string): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
const sessionWarningsPulled = new Set<string>();
|
||||
|
||||
async function pullSessionWarnings(sessionId: string): Promise<void> {
|
||||
if (sessionWarningsPulled.has(sessionId)) return;
|
||||
sessionWarningsPulled.add(sessionId);
|
||||
try {
|
||||
const warnings = await getKimiWebApi().getSessionWarnings(sessionId);
|
||||
const label = i18n.global.t('warnings.noteLabel');
|
||||
for (const warning of warnings) {
|
||||
pushWarning(`${label}: ${warning.message}`);
|
||||
}
|
||||
} catch {
|
||||
// best-effort: never block session sync on warning retrieval.
|
||||
}
|
||||
}
|
||||
|
||||
async function syncSessionFromSnapshot(sessionId: string): Promise<SyncSessionResult> {
|
||||
try {
|
||||
const api = getKimiWebApi();
|
||||
|
|
@ -977,6 +993,7 @@ async function syncSessionFromSnapshot(sessionId: string): Promise<SyncSessionRe
|
|||
eventConn.seedSnapshot(sessionId, snap);
|
||||
eventConn.subscribe(sessionId, { seq: snap.asOfSeq, epoch: snap.epoch });
|
||||
}
|
||||
void pullSessionWarnings(sessionId);
|
||||
return 'ok';
|
||||
} catch (err) {
|
||||
if (isSessionNotFoundError(err)) {
|
||||
|
|
|
|||
|
|
@ -6,16 +6,21 @@ import { normalizeAdditionalDirs } from '../config';
|
|||
import { listDirectory } from '../tools/support/list-directory';
|
||||
import type { SystemPromptContext } from './types';
|
||||
|
||||
const AGENTS_MD_MAX_BYTES = 32 * 1024;
|
||||
const AGENTS_MD_TRUNCATION_MARKER =
|
||||
'<!-- Some AGENTS.md files were truncated or omitted to fit the 32 KB budget -->';
|
||||
// Soft budget for the combined AGENTS.md content injected into the system
|
||||
// prompt. ~32 KB is roughly 8K–20K tokens (≈1.5–3% of a 262144-token context),
|
||||
// large enough to leave the bulk of the context window to the conversation
|
||||
// while still catching accidental oversized instruction files. Exceeding it no
|
||||
// longer truncates content; it only surfaces a user-visible warning so the user
|
||||
// can trim oversized instruction files.
|
||||
const AGENTS_MD_RECOMMENDED_MAX_BYTES = 32 * 1024;
|
||||
const S_IFMT = 0o170000;
|
||||
const S_IFREG = 0o100000;
|
||||
|
||||
export type PreparedSystemPromptContext = Pick<
|
||||
SystemPromptContext,
|
||||
'cwdListing' | 'agentsMd' | 'additionalDirsInfo'
|
||||
>;
|
||||
export interface PreparedSystemPromptContext
|
||||
extends Pick<SystemPromptContext, 'cwdListing' | 'agentsMd' | 'additionalDirsInfo'> {
|
||||
/** Present when the combined AGENTS.md content exceeds the recommended size. */
|
||||
readonly agentsMdWarning?: string;
|
||||
}
|
||||
|
||||
export interface PrepareSystemPromptContextOptions {
|
||||
readonly additionalDirs?: readonly string[];
|
||||
|
|
@ -27,23 +32,34 @@ export async function prepareSystemPromptContext(
|
|||
options?: PrepareSystemPromptContextOptions,
|
||||
): Promise<PreparedSystemPromptContext> {
|
||||
const additionalDirs = normalizeAdditionalDirs(options?.additionalDirs ?? []);
|
||||
const [cwdListing, agentsMd, additionalDirsInfo] = await Promise.all([
|
||||
const [cwdListing, agentsMdResult, additionalDirsInfo] = await Promise.all([
|
||||
listDirectory(kaos, undefined, { collapseHiddenDirs: true }),
|
||||
loadAgentsMd(kaos, brandHome),
|
||||
loadAgentsMdForRoots(kaos, brandHome, [kaos.getcwd()]),
|
||||
loadAdditionalDirsInfo(kaos, additionalDirs),
|
||||
]);
|
||||
return { cwdListing, agentsMd, additionalDirsInfo };
|
||||
return {
|
||||
cwdListing,
|
||||
agentsMd: agentsMdResult.content,
|
||||
additionalDirsInfo,
|
||||
agentsMdWarning: agentsMdResult.warning,
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadAgentsMd(kaos: Kaos, brandHome?: string): Promise<string> {
|
||||
return loadAgentsMdForRoots(kaos, brandHome, [kaos.getcwd()]);
|
||||
const result = await loadAgentsMdForRoots(kaos, brandHome, [kaos.getcwd()]);
|
||||
return result.content;
|
||||
}
|
||||
|
||||
interface LoadedAgentsMd {
|
||||
readonly content: string;
|
||||
readonly warning: string | undefined;
|
||||
}
|
||||
|
||||
async function loadAgentsMdForRoots(
|
||||
kaos: Kaos,
|
||||
brandHome: string | undefined,
|
||||
workDirs: readonly string[],
|
||||
): Promise<string> {
|
||||
): Promise<LoadedAgentsMd> {
|
||||
const discovered: AgentFile[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
|
|
@ -87,7 +103,15 @@ async function loadAgentsMdForRoots(
|
|||
}
|
||||
}
|
||||
|
||||
return renderAgentFiles(discovered);
|
||||
const content = renderAgentFiles(discovered);
|
||||
const totalBytes = byteLength(content);
|
||||
const warning =
|
||||
totalBytes > AGENTS_MD_RECOMMENDED_MAX_BYTES
|
||||
? `AGENTS.md total ${formatKB(totalBytes)} KB exceeds the recommended ` +
|
||||
`${formatKB(AGENTS_MD_RECOMMENDED_MAX_BYTES)} KB. Large instruction files ` +
|
||||
`increase cost and may impact performance; consider trimming.`
|
||||
: undefined;
|
||||
return { content, warning };
|
||||
}
|
||||
|
||||
async function loadAdditionalDirsInfo(
|
||||
|
|
@ -163,77 +187,18 @@ async function isFile(kaos: Kaos, path: string): Promise<boolean> {
|
|||
|
||||
function renderAgentFiles(files: readonly AgentFile[]): string {
|
||||
if (files.length === 0) return '';
|
||||
|
||||
let remaining = AGENTS_MD_MAX_BYTES;
|
||||
let didTruncate = false;
|
||||
const budgeted: Array<AgentFile | undefined> = Array.from({ length: files.length });
|
||||
|
||||
for (let i = files.length - 1; i >= 0; i--) {
|
||||
const file = files[i];
|
||||
if (file === undefined) continue;
|
||||
|
||||
const annotation = annotationFor(file.path);
|
||||
const separator = i < files.length - 1 ? '\n\n' : '';
|
||||
remaining -= byteLength(annotation) + byteLength(separator);
|
||||
if (remaining <= 0) {
|
||||
budgeted[i] = { path: file.path, content: '' };
|
||||
remaining = 0;
|
||||
didTruncate = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
let content = file.content;
|
||||
if (byteLength(content) > remaining) {
|
||||
content = truncateUtf8(content, remaining).trim();
|
||||
didTruncate = true;
|
||||
}
|
||||
remaining -= byteLength(content);
|
||||
budgeted[i] = { path: file.path, content };
|
||||
}
|
||||
|
||||
const rendered = budgeted
|
||||
.filter((file): file is AgentFile => file !== undefined && file.content.length > 0)
|
||||
.map((file) => `${annotationFor(file.path)}${file.content}`)
|
||||
.join('\n\n');
|
||||
|
||||
return didTruncate ? `${AGENTS_MD_TRUNCATION_MARKER}\n${rendered}` : rendered;
|
||||
}
|
||||
|
||||
function truncateUtf8(text: string, maxBytes: number): string {
|
||||
if (maxBytes <= 0) return '';
|
||||
if (byteLength(text) <= maxBytes) return text;
|
||||
|
||||
let low = 0;
|
||||
let high = text.length;
|
||||
while (low < high) {
|
||||
const mid = Math.ceil((low + high) / 2);
|
||||
const candidate = text.slice(0, mid);
|
||||
if (byteLength(candidate) <= maxBytes) {
|
||||
low = mid;
|
||||
} else {
|
||||
high = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
let result = text.slice(0, low);
|
||||
while (endsWithUnpairedHighSurrogate(result)) {
|
||||
result = result.slice(0, -1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function endsWithUnpairedHighSurrogate(text: string): boolean {
|
||||
if (text.length === 0) return false;
|
||||
const codePoint = text.codePointAt(text.length - 1);
|
||||
return codePoint !== undefined && codePoint >= 0xd800 && codePoint <= 0xdbff;
|
||||
return files.map((file) => `${annotationFor(file.path)}${file.content}`).join('\n\n');
|
||||
}
|
||||
|
||||
function byteLength(text: string): number {
|
||||
return Buffer.byteLength(text, 'utf8');
|
||||
}
|
||||
|
||||
function formatKB(bytes: number): string {
|
||||
const kb = bytes / 1024;
|
||||
return Number.isInteger(kb) ? String(kb) : kb.toFixed(1);
|
||||
}
|
||||
|
||||
function annotationFor(path: string): string {
|
||||
return `<!-- From: ${path} -->\n`;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import type { ExperimentalFeatureState } from '#/flags';
|
|||
import type { ResumeSessionResult } from '#/rpc/resumed';
|
||||
import type { SessionMeta } from '#/session';
|
||||
import type { ContentPart } from '@moonshot-ai/kosong';
|
||||
import type { SessionWarning } from '@moonshot-ai/protocol';
|
||||
|
||||
import type { PluginInfo, PluginSummary, ReloadSummary } from '#/plugin';
|
||||
import type { UsageStatus } from './events';
|
||||
|
|
@ -387,6 +388,7 @@ export interface SessionAPI extends AgentAPIWithId {
|
|||
getMcpStartupMetrics: (payload: EmptyPayload) => McpStartupMetrics;
|
||||
reconnectMcpServer: (payload: ReconnectMcpServerPayload) => void;
|
||||
generateAgentsMd: (payload: EmptyPayload) => void;
|
||||
getSessionWarnings: (payload: EmptyPayload) => readonly SessionWarning[];
|
||||
addAdditionalDir: (payload: AddAdditionalDirPayload) => AddAdditionalDirResult;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ import type {
|
|||
} from './core-api';
|
||||
import type { ResumedAgentState, ResumeSessionResult } from './resumed';
|
||||
import type { SDKRPC } from './sdk-api';
|
||||
import type { SessionWarning } from '@moonshot-ai/protocol';
|
||||
import { proxyWithExtraPayload } from './types';
|
||||
import { KaosShellNotFoundError, LocalKaos, type Kaos } from '@moonshot-ai/kaos';
|
||||
import type { ToolServices } from '../tools/support/services';
|
||||
|
|
@ -742,6 +743,10 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
|
|||
return this.sessionApi(sessionId).generateAgentsMd(payload);
|
||||
}
|
||||
|
||||
getSessionWarnings({ sessionId, ...payload }: SessionScopedPayload<EmptyPayload>): Promise<readonly SessionWarning[]> {
|
||||
return this.sessionApi(sessionId).getSessionWarnings(payload);
|
||||
}
|
||||
|
||||
addAdditionalDir({
|
||||
sessionId,
|
||||
...payload
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
type SessionCreate,
|
||||
type SessionFork,
|
||||
type SessionStatusResponse,
|
||||
type SessionWarning,
|
||||
type SessionUpdate,
|
||||
type UndoSessionRequest,
|
||||
type UndoSessionResponse,
|
||||
|
|
@ -55,6 +56,8 @@ export interface ISessionService {
|
|||
|
||||
getStatus(id: string): Promise<SessionStatusResponse>;
|
||||
|
||||
getSessionWarnings(id: string): Promise<readonly SessionWarning[]>;
|
||||
|
||||
compact(id: string, input: CompactSessionRequest): Promise<CompactSessionResponse>;
|
||||
|
||||
undo(id: string, input: UndoSessionRequest): Promise<UndoSessionResponse>;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
type SessionStatus,
|
||||
type SessionStatusResponse,
|
||||
type SessionUpdate,
|
||||
type SessionWarning,
|
||||
type UndoSessionRequest,
|
||||
type UndoSessionResponse,
|
||||
} from '@moonshot-ai/protocol';
|
||||
|
|
@ -474,6 +475,23 @@ export class SessionService extends Disposable implements ISessionService {
|
|||
};
|
||||
}
|
||||
|
||||
async getSessionWarnings(id: string): Promise<readonly SessionWarning[]> {
|
||||
const all = await this.core.rpc.listSessions({});
|
||||
if (!all.some((s) => s.id === id)) {
|
||||
throw new SessionNotFoundError(id);
|
||||
}
|
||||
try {
|
||||
await this.core.rpc.resumeSession({ sessionId: id });
|
||||
} catch {
|
||||
// best-effort: the session may already be loaded in core memory.
|
||||
}
|
||||
try {
|
||||
return await this.core.rpc.getSessionWarnings({ sessionId: id });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async compact(id: string, input: CompactSessionRequest): Promise<CompactSessionResponse> {
|
||||
const all = await this.core.rpc.listSessions({});
|
||||
const summary = all.find((s) => s.id === id);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { homedir } from 'node:os';
|
||||
import { join } from 'pathe';
|
||||
import type { Kaos } from '@moonshot-ai/kaos';
|
||||
import type { SessionWarning } from '@moonshot-ai/protocol';
|
||||
|
||||
import { ErrorCodes, KimiError } from '#/errors';
|
||||
import { getRootLogger, log } from '#/logging/logger';
|
||||
|
|
@ -169,6 +170,7 @@ export class Session {
|
|||
custom: {},
|
||||
};
|
||||
private writeMetadataPromise = Promise.resolve();
|
||||
private agentsMdWarning: string | undefined;
|
||||
|
||||
constructor(public readonly options: SessionOptions) {
|
||||
// Attach the per-session log sink up front so the constructor's
|
||||
|
|
@ -467,6 +469,49 @@ export class Session {
|
|||
{ additionalDirs: this.additionalDirs },
|
||||
);
|
||||
agent.useProfile(profile, context);
|
||||
const { agentsMdWarning } = context;
|
||||
if (agentsMdWarning !== undefined) {
|
||||
this.agentsMdWarning = agentsMdWarning;
|
||||
log.warn('AGENTS.md exceeds recommended size', { message: agentsMdWarning });
|
||||
agent.emitEvent({
|
||||
type: 'warning',
|
||||
message: agentsMdWarning,
|
||||
code: 'agents-md-oversized',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getSessionWarnings(): Promise<readonly SessionWarning[]> {
|
||||
const warnings: SessionWarning[] = [];
|
||||
const agentsMdWarning = await this.computeAgentsMdWarning();
|
||||
if (agentsMdWarning !== undefined) {
|
||||
warnings.push({
|
||||
code: 'agents-md-oversized',
|
||||
message: agentsMdWarning,
|
||||
severity: 'warning',
|
||||
});
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
private async computeAgentsMdWarning(): Promise<string | undefined> {
|
||||
if (this.agentsMdWarning !== undefined) {
|
||||
return this.agentsMdWarning;
|
||||
}
|
||||
// Resumed sessions skip bootstrap when their system prompt is already set, so
|
||||
// the cached value may be missing; recompute on demand so the warning still
|
||||
// surfaces for long-lived sessions.
|
||||
try {
|
||||
const context = await prepareSystemPromptContext(
|
||||
this.systemContextKaos(this.toolKaos.getcwd()),
|
||||
this.options.kimiHomeDir,
|
||||
{ additionalDirs: this.additionalDirs },
|
||||
);
|
||||
this.agentsMdWarning = context.agentsMdWarning;
|
||||
} catch (error) {
|
||||
log.warn('failed to compute AGENTS.md warning', { error });
|
||||
}
|
||||
return this.agentsMdWarning;
|
||||
}
|
||||
|
||||
async generateAgentsMd(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ErrorCodes, KimiError } from '#/errors';
|
||||
import type { SessionWarning } from '@moonshot-ai/protocol';
|
||||
import type {
|
||||
ActivateSkillPayload,
|
||||
AddAdditionalDirPayload,
|
||||
|
|
@ -93,6 +94,10 @@ export class SessionAPIImpl implements PromisableMethods<SessionAPI> {
|
|||
return this.session.generateAgentsMd();
|
||||
}
|
||||
|
||||
getSessionWarnings(_payload: EmptyPayload): Promise<readonly SessionWarning[]> {
|
||||
return this.session.getSessionWarnings();
|
||||
}
|
||||
|
||||
addAdditionalDir(payload: AddAdditionalDirPayload): Promise<AddAdditionalDirResult> {
|
||||
return this.session.addAdditionalDir(payload.path, payload.persist);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,16 +115,40 @@ describe('loadAgentsMd brand home (KIMI_CODE_HOME)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('loadAgentsMd truncation marker', () => {
|
||||
it('adds a marker when AGENTS.md content is truncated', async () => {
|
||||
describe('loadAgentsMd oversized content', () => {
|
||||
it('keeps the full content when AGENTS.md exceeds the recommended size', async () => {
|
||||
const largeContent = 'x'.repeat(40 * 1024);
|
||||
await writeFile(join(workDir, 'AGENTS.md'), largeContent, 'utf-8');
|
||||
|
||||
const result = await loadAgentsMd(testKaos);
|
||||
|
||||
expect(result).toContain('Some AGENTS.md files were truncated or omitted');
|
||||
expect(result).toContain(`<!-- From: ${join(workDir, 'AGENTS.md')} -->`);
|
||||
expect(result).not.toContain(largeContent);
|
||||
expect(result).toContain(largeContent);
|
||||
expect(result).not.toContain('truncated or omitted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepareSystemPromptContext AGENTS.md size warning', () => {
|
||||
it('returns agentsMdWarning and keeps full content when oversized', async () => {
|
||||
const brandHome = await mkdtemp(join(tmpdir(), 'kimi-agents-brand-'));
|
||||
extraDirs.push(brandHome);
|
||||
const largeContent = 'x'.repeat(40 * 1024);
|
||||
await writeFile(join(workDir, 'AGENTS.md'), largeContent, 'utf-8');
|
||||
|
||||
const result = await prepareSystemPromptContext(testKaos, brandHome);
|
||||
|
||||
expect(result.agentsMd).toContain(largeContent);
|
||||
expect(result.agentsMdWarning).toBeDefined();
|
||||
expect(result.agentsMdWarning).toContain('exceeds the recommended');
|
||||
});
|
||||
|
||||
it('does not return agentsMdWarning when within the recommended size', async () => {
|
||||
const brandHome = await mkdtemp(join(tmpdir(), 'kimi-agents-brand-'));
|
||||
extraDirs.push(brandHome);
|
||||
await writeFile(join(workDir, 'AGENTS.md'), 'small instructions', 'utf-8');
|
||||
|
||||
const result = await prepareSystemPromptContext(testKaos, brandHome);
|
||||
|
||||
expect(result.agentsMdWarning).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -309,6 +309,7 @@ function makeSessionService(): {
|
|||
listChildren: vi.fn() as unknown as ISessionService['listChildren'],
|
||||
createChild: vi.fn() as unknown as ISessionService['createChild'],
|
||||
getStatus: vi.fn() as unknown as ISessionService['getStatus'],
|
||||
getSessionWarnings: vi.fn() as unknown as ISessionService['getSessionWarnings'],
|
||||
compact: vi.fn() as unknown as ISessionService['compact'],
|
||||
undo: vi.fn() as unknown as ISessionService['undo'],
|
||||
archive: vi.fn() as unknown as ISessionService['archive'],
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ function makeSessionService(sessions: Map<string, Session>): ISessionService {
|
|||
getStatus: async () => {
|
||||
throw new Error('not implemented');
|
||||
},
|
||||
getSessionWarnings: async () => [],
|
||||
compact: async () => {
|
||||
throw new Error('not implemented');
|
||||
},
|
||||
|
|
|
|||
|
|
@ -245,6 +245,11 @@ export abstract class SDKRpcClientBase {
|
|||
return rpc.generateAgentsMd({ sessionId: input.sessionId });
|
||||
}
|
||||
|
||||
async getSessionWarnings(input: SessionIdRpcInput) {
|
||||
const rpc = await this.getRpc();
|
||||
return rpc.getSessionWarnings({ sessionId: input.sessionId });
|
||||
}
|
||||
|
||||
async addAdditionalDir(input: AddAdditionalDirInput): Promise<AddAdditionalDirResult> {
|
||||
const rpc = await this.getRpc();
|
||||
return rpc.addAdditionalDir({ sessionId: input.id, path: input.path, persist: input.persist });
|
||||
|
|
|
|||
|
|
@ -124,6 +124,11 @@ export class Session {
|
|||
await this.rpc.generateAgentsMd({ sessionId: this.id });
|
||||
}
|
||||
|
||||
async getSessionWarnings() {
|
||||
this.ensureOpen();
|
||||
return this.rpc.getSessionWarnings({ sessionId: this.id });
|
||||
}
|
||||
|
||||
async addAdditionalDir(
|
||||
path: string,
|
||||
options?: AddAdditionalDirOptions,
|
||||
|
|
|
|||
|
|
@ -110,6 +110,18 @@ export const sessionStatusResponseSchema = z.object({
|
|||
});
|
||||
export type SessionStatusResponse = z.infer<typeof sessionStatusResponseSchema>;
|
||||
|
||||
export const sessionWarningSchema = z.object({
|
||||
code: z.string(),
|
||||
message: z.string(),
|
||||
severity: z.enum(['info', 'warning', 'error']),
|
||||
});
|
||||
export type SessionWarning = z.infer<typeof sessionWarningSchema>;
|
||||
|
||||
export const sessionWarningsResponseSchema = z.object({
|
||||
warnings: z.array(sessionWarningSchema),
|
||||
});
|
||||
export type SessionWarningsResponse = z.infer<typeof sessionWarningsResponseSchema>;
|
||||
|
||||
export const compactSessionRequestSchema = z.preprocess(
|
||||
(value) => value === undefined ? {} : value,
|
||||
z.object({
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
pageResponseSchema,
|
||||
sessionAbortResponseSchema,
|
||||
sessionSchema,
|
||||
sessionWarningsResponseSchema,
|
||||
sessionStatusResponseSchema,
|
||||
sessionStatusSchema,
|
||||
startBtwSessionResponseSchema,
|
||||
|
|
@ -575,6 +576,37 @@ export function registerSessionsRoutes(
|
|||
);
|
||||
app.get(statusRoute.path, statusRoute.options, statusRoute.handler as Parameters<SessionRouteHost['get']>[2]);
|
||||
|
||||
const sessionWarningsRoute = defineRoute(
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/sessions/{session_id}/warnings',
|
||||
params: sessionIdParamSchema,
|
||||
success: { data: sessionWarningsResponseSchema },
|
||||
errors: {
|
||||
[ErrorCode.VALIDATION_FAILED]: { detailsSchema },
|
||||
[ErrorCode.SESSION_NOT_FOUND]: {},
|
||||
},
|
||||
description: 'Get session-level warnings (e.g. oversized AGENTS.md)',
|
||||
tags: ['sessions'],
|
||||
},
|
||||
async (req, reply) => {
|
||||
try {
|
||||
const { session_id } = req.params;
|
||||
const warnings = await ix.invokeFunction((a) =>
|
||||
a.get(ISessionService).getSessionWarnings(session_id),
|
||||
);
|
||||
reply.send(okEnvelope({ warnings }, req.id));
|
||||
} catch (err) {
|
||||
sendMappedError(reply, req.id, err);
|
||||
}
|
||||
},
|
||||
);
|
||||
app.get(
|
||||
sessionWarningsRoute.path,
|
||||
sessionWarningsRoute.options,
|
||||
sessionWarningsRoute.handler as Parameters<SessionRouteHost['get']>[2],
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
function sendMappedError(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue