From d088ec885059df2bce4d58778e6338753517e1a0 Mon Sep 17 00:00:00 2001 From: Kaiyi Date: Tue, 23 Jun 2026 21:07:40 +0800 Subject: [PATCH] feat: replace silent AGENTS.md truncation with a visible warning 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. --- .changeset/soft-agents-md-warning.md | 5 + apps/kimi-code/src/tui/kimi-tui.ts | 17 +++ apps/kimi-web/src/api/daemon/client.ts | 9 ++ apps/kimi-web/src/api/daemon/wire.ts | 11 ++ apps/kimi-web/src/api/types.ts | 7 + .../src/composables/useKimiWebClient.ts | 17 +++ packages/agent-core/src/profile/context.ts | 121 +++++++----------- packages/agent-core/src/rpc/core-api.ts | 2 + packages/agent-core/src/rpc/core-impl.ts | 5 + .../src/services/session/session.ts | 3 + .../src/services/session/sessionService.ts | 18 +++ packages/agent-core/src/session/index.ts | 45 +++++++ packages/agent-core/src/session/rpc.ts | 5 + .../agent-core/test/profile/context.test.ts | 34 ++++- .../test/services/prompt-service.test.ts | 1 + .../test/services/terminal-service.test.ts | 1 + packages/node-sdk/src/rpc.ts | 5 + packages/node-sdk/src/session.ts | 5 + packages/protocol/src/rest/session.ts | 12 ++ packages/server/src/routes/sessions.ts | 32 +++++ 20 files changed, 272 insertions(+), 83 deletions(-) create mode 100644 .changeset/soft-agents-md-warning.md diff --git a/.changeset/soft-agents-md-warning.md b/.changeset/soft-agents-md-warning.md new file mode 100644 index 0000000000..c9dd4b7a1c --- /dev/null +++ b/.changeset/soft-agents-md-warning.md @@ -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. diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index c7f3951ec1..57c6f15e0f 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -559,6 +559,7 @@ export class KimiTUI { } if (this.session !== undefined) { this.sessionEventHandler.startSubscription(); + void this.showSessionWarnings(this.session); } void this.fetchSessions(); if (this.session !== undefined) { @@ -567,6 +568,19 @@ export class KimiTUI { void this.refreshSkillCommands(this.session); } + private async showSessionWarnings(session: Session): Promise { + 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 { const warning = await detectTmuxKeyboardWarning(); if (warning === undefined || this.aborted) return; @@ -1355,6 +1369,7 @@ export class KimiTUI { this.showStatus(`Warning: ${resumeState.warning}`, 'warning'); } this.showStatus(statusMessage); + void this.showSessionWarnings(session); } async reloadCurrentSessionView(session: Session, statusMessage: string): Promise { @@ -1383,6 +1398,7 @@ export class KimiTUI { this.showStatus(`Warning: ${resumeState.warning}`, 'warning'); } this.showStatus(statusMessage); + void this.showSessionWarnings(session); } async createNewSession(): Promise { @@ -1420,6 +1436,7 @@ export class KimiTUI { this.sessionEventHandler.startSubscription(); this.clearTranscriptAndRedraw(); this.showStatus(`Started a new session (${session.id}).`); + void this.showSessionWarnings(session); void this.showConfigWarningsIfAny(); } diff --git a/apps/kimi-web/src/api/daemon/client.ts b/apps/kimi-web/src/api/daemon/client.ts index 97a92f8c2c..691bc85d6f 100644 --- a/apps/kimi-web/src/api/daemon/client.ts +++ b/apps/kimi-web/src/api/daemon/client.ts @@ -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 { + const data = await this.http.get( + `/sessions/${encodeURIComponent(sessionId)}/warnings`, + ); + return data.warnings ?? []; + } + async archiveSession(sessionId: string): Promise<{ archived: true }> { const data = await this.http.post( `/sessions/${encodeURIComponent(sessionId)}:archive`, diff --git a/apps/kimi-web/src/api/daemon/wire.ts b/apps/kimi-web/src/api/daemon/wire.ts index f2f9195e83..314cb96cfe 100644 --- a/apps/kimi-web/src/api/daemon/wire.ts +++ b/apps/kimi-web/src/api/daemon/wire.ts @@ -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. diff --git a/apps/kimi-web/src/api/types.ts b/apps/kimi-web/src/api/types.ts index ca243fd151..ad52b6e0d8 100644 --- a/apps/kimi-web/src/api/types.ts +++ b/apps/kimi-web/src/api/types.ts @@ -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; openInApps: string[] }>; @@ -607,6 +613,7 @@ export interface KimiWebApi { getSession(sessionId: string): Promise; 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; getSessionStatus(sessionId: string): Promise; + getSessionWarnings(sessionId: string): Promise; archiveSession(sessionId: string): Promise<{ archived: true }>; listMessages(sessionId: string, input?: PageRequest & { role?: AppMessageRole }): Promise>; /** v2 initial sync: atomic session state + `asOfSeq` watermark + epoch. */ diff --git a/apps/kimi-web/src/composables/useKimiWebClient.ts b/apps/kimi-web/src/composables/useKimiWebClient.ts index 1f04a3f563..070a3a1372 100644 --- a/apps/kimi-web/src/composables/useKimiWebClient.ts +++ b/apps/kimi-web/src/composables/useKimiWebClient.ts @@ -942,6 +942,22 @@ async function handleSessionNotFound(sessionId: string): Promise { } } +const sessionWarningsPulled = new Set(); + +async function pullSessionWarnings(sessionId: string): Promise { + 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 { try { const api = getKimiWebApi(); @@ -980,6 +996,7 @@ async function syncSessionFromSnapshot(sessionId: string): Promise'; +// 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 { + /** 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 { 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 { - 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 { +): Promise { const discovered: AgentFile[] = []; const seen = new Set(); @@ -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 { function renderAgentFiles(files: readonly AgentFile[]): string { if (files.length === 0) return ''; - - let remaining = AGENTS_MD_MAX_BYTES; - let didTruncate = false; - const budgeted: Array = 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 `\n`; } - - diff --git a/packages/agent-core/src/rpc/core-api.ts b/packages/agent-core/src/rpc/core-api.ts index 2b7aaf54f5..61b19e5693 100644 --- a/packages/agent-core/src/rpc/core-api.ts +++ b/packages/agent-core/src/rpc/core-api.ts @@ -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; } diff --git a/packages/agent-core/src/rpc/core-impl.ts b/packages/agent-core/src/rpc/core-impl.ts index 54e07fd24d..7088fba232 100644 --- a/packages/agent-core/src/rpc/core-impl.ts +++ b/packages/agent-core/src/rpc/core-impl.ts @@ -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 { return this.sessionApi(sessionId).generateAgentsMd(payload); } + getSessionWarnings({ sessionId, ...payload }: SessionScopedPayload): Promise { + return this.sessionApi(sessionId).getSessionWarnings(payload); + } + addAdditionalDir({ sessionId, ...payload diff --git a/packages/agent-core/src/services/session/session.ts b/packages/agent-core/src/services/session/session.ts index 798b207f39..1d83feead3 100644 --- a/packages/agent-core/src/services/session/session.ts +++ b/packages/agent-core/src/services/session/session.ts @@ -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; + getSessionWarnings(id: string): Promise; + compact(id: string, input: CompactSessionRequest): Promise; undo(id: string, input: UndoSessionRequest): Promise; diff --git a/packages/agent-core/src/services/session/sessionService.ts b/packages/agent-core/src/services/session/sessionService.ts index a57908bcab..cf98d3ee47 100644 --- a/packages/agent-core/src/services/session/sessionService.ts +++ b/packages/agent-core/src/services/session/sessionService.ts @@ -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 { + 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 { const all = await this.core.rpc.listSessions({}); const summary = all.find((s) => s.id === id); diff --git a/packages/agent-core/src/session/index.ts b/packages/agent-core/src/session/index.ts index 5ebf1eecd6..bc6642151f 100644 --- a/packages/agent-core/src/session/index.ts +++ b/packages/agent-core/src/session/index.ts @@ -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 { + 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 { + 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 { diff --git a/packages/agent-core/src/session/rpc.ts b/packages/agent-core/src/session/rpc.ts index cddfe967b6..05d6c52770 100644 --- a/packages/agent-core/src/session/rpc.ts +++ b/packages/agent-core/src/session/rpc.ts @@ -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 { return this.session.generateAgentsMd(); } + getSessionWarnings(_payload: EmptyPayload): Promise { + return this.session.getSessionWarnings(); + } + addAdditionalDir(payload: AddAdditionalDirPayload): Promise { return this.session.addAdditionalDir(payload.path, payload.persist); } diff --git a/packages/agent-core/test/profile/context.test.ts b/packages/agent-core/test/profile/context.test.ts index f7a6bfa5c5..41eeb8f3cc 100644 --- a/packages/agent-core/test/profile/context.test.ts +++ b/packages/agent-core/test/profile/context.test.ts @@ -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(``); - 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(); }); }); diff --git a/packages/agent-core/test/services/prompt-service.test.ts b/packages/agent-core/test/services/prompt-service.test.ts index e988469fc9..6c563235f3 100644 --- a/packages/agent-core/test/services/prompt-service.test.ts +++ b/packages/agent-core/test/services/prompt-service.test.ts @@ -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'], diff --git a/packages/agent-core/test/services/terminal-service.test.ts b/packages/agent-core/test/services/terminal-service.test.ts index 5874eff4f7..b5423c2e00 100644 --- a/packages/agent-core/test/services/terminal-service.test.ts +++ b/packages/agent-core/test/services/terminal-service.test.ts @@ -136,6 +136,7 @@ function makeSessionService(sessions: Map): ISessionService { getStatus: async () => { throw new Error('not implemented'); }, + getSessionWarnings: async () => [], compact: async () => { throw new Error('not implemented'); }, diff --git a/packages/node-sdk/src/rpc.ts b/packages/node-sdk/src/rpc.ts index 2b088df9ef..7582bec647 100644 --- a/packages/node-sdk/src/rpc.ts +++ b/packages/node-sdk/src/rpc.ts @@ -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 { const rpc = await this.getRpc(); return rpc.addAdditionalDir({ sessionId: input.id, path: input.path, persist: input.persist }); diff --git a/packages/node-sdk/src/session.ts b/packages/node-sdk/src/session.ts index 8995297607..b6b2eb9ee4 100644 --- a/packages/node-sdk/src/session.ts +++ b/packages/node-sdk/src/session.ts @@ -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, diff --git a/packages/protocol/src/rest/session.ts b/packages/protocol/src/rest/session.ts index 68f2dda334..a6d7bacc47 100644 --- a/packages/protocol/src/rest/session.ts +++ b/packages/protocol/src/rest/session.ts @@ -110,6 +110,18 @@ export const sessionStatusResponseSchema = z.object({ }); export type SessionStatusResponse = z.infer; +export const sessionWarningSchema = z.object({ + code: z.string(), + message: z.string(), + severity: z.enum(['info', 'warning', 'error']), +}); +export type SessionWarning = z.infer; + +export const sessionWarningsResponseSchema = z.object({ + warnings: z.array(sessionWarningSchema), +}); +export type SessionWarningsResponse = z.infer; + export const compactSessionRequestSchema = z.preprocess( (value) => value === undefined ? {} : value, z.object({ diff --git a/packages/server/src/routes/sessions.ts b/packages/server/src/routes/sessions.ts index 8ccabb0b36..3f3cf0c028 100644 --- a/packages/server/src/routes/sessions.ts +++ b/packages/server/src/routes/sessions.ts @@ -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[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[2], + ); + } function sendMappedError(