Skip to content

Commit e56d571

Browse files
committed
fix(agent-core): scope [image] config limits to the owning core
1 parent 1bf2c9a commit e56d571

16 files changed

Lines changed: 310 additions & 84 deletions

File tree

.changeset/scoped-image-limits.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@moonshot-ai/kimi-code": patch
3+
---
4+
5+
Scope `[image]` config limits to the owning core instead of a process-global value, so multiple cores in one process (the SDK's multi-client pattern) each compress images with their own `max_edge_px` / `read_byte_budget`, and a config reload of one core never retunes another; the env vars `KIMI_IMAGE_MAX_EDGE_PX` / `KIMI_IMAGE_READ_BYTE_BUDGET` still override process-wide.

packages/agent-core/src/agent/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { PluginCommandOrigin } from './context';
1414

1515
import type { McpConnectionManager } from '../mcp';
1616
import { FlagResolver, type ExperimentalFlagResolver } from '../flags';
17+
import { ImageLimits } from '../tools/support/image-limits';
1718
import {
1819
prepareSystemPromptContext,
1920
type PreparedSystemPromptContext,
@@ -96,6 +97,8 @@ export interface AgentOptions {
9697
readonly pluginSessionStarts?: readonly EnabledPluginSessionStart[];
9798
readonly pluginCommands?: readonly PluginCommandDef[];
9899
readonly experimentalFlags?: ExperimentalFlagResolver;
100+
/** Owner-scoped [image] limits; a standalone Agent gets env/built-in defaults. */
101+
readonly imageLimits?: ImageLimits;
99102
readonly replay?: ReplayBuilderOptions;
100103
readonly additionalDirs?: readonly string[];
101104
readonly systemPromptContextProvider?: (() => Promise<PreparedSystemPromptContext>) | undefined;
@@ -124,6 +127,7 @@ export class Agent {
124127
readonly log: Logger;
125128
readonly telemetry: TelemetryClient;
126129
readonly experimentalFlags: ExperimentalFlagResolver;
130+
readonly imageLimits: ImageLimits;
127131

128132
readonly llmRequestLogger: LlmRequestLogger;
129133
readonly llmRequestRecorder: LlmRequestRecorder;
@@ -178,6 +182,7 @@ export class Agent {
178182
this.log = options.log ?? log;
179183
this.telemetry = options.telemetry ?? noopTelemetryClient;
180184
this.experimentalFlags = options.experimentalFlags ?? new FlagResolver();
185+
this.imageLimits = options.imageLimits ?? new ImageLimits();
181186
this.additionalDirs = normalizeAdditionalDirs(options.additionalDirs ?? []);
182187
this.systemPromptContextProvider = options.systemPromptContextProvider;
183188

packages/agent-core/src/agent/tool/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@ export class ToolManager {
317317
return mcpResultToExecutableOutput(result, qualified, {
318318
originalsDir: this.agent.mediaOriginalsDir,
319319
telemetry: this.agent.telemetry,
320+
// Resolved per call so a config reload applies immediately.
321+
maxImageEdgePx: this.agent.imageLimits?.maxEdgePx(),
320322
});
321323
},
322324
};
@@ -706,6 +708,7 @@ export class ToolManager {
706708
modelCapabilities,
707709
videoUploader,
708710
this.agent.telemetry,
711+
this.agent.imageLimits,
709712
),
710713
new b.EnterPlanModeTool(this.agent),
711714
new b.ExitPlanModeTool(this.agent),

packages/agent-core/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,13 @@ export {
6464
compressImageContentParts,
6565
cropImageForModel,
6666
formatByteSize,
67+
resolveMaxImageEdgePx,
68+
resolveReadImageByteBudget,
6769
IMAGE_BYTE_BUDGET,
6870
MAX_IMAGE_EDGE_PX,
71+
READ_IMAGE_BYTE_BUDGET,
6972
} from './tools/support/image-compress';
73+
export { ImageLimits } from './tools/support/image-limits';
7074
export type {
7175
CompressAnnotateOptions,
7276
CompressedContentParts,

packages/agent-core/src/mcp/output.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export interface McpOutputOptions {
4444
readonly originalsDir?: string | undefined;
4545
/** Report an `image_compress` event per compressed tool-result image. */
4646
readonly telemetry?: TelemetryClient | undefined;
47+
/** Owner-resolved longest-edge ceiling (px) for tool-result images. */
48+
readonly maxImageEdgePx?: number | undefined;
4749
}
4850

4951
// MCP servers can produce arbitrarily large outputs; cap what we feed back to
@@ -188,6 +190,7 @@ export async function mcpResultToExecutableOutput(
188190
// DATA (never inserted into the parts), so tool output that merely quotes
189191
// a caption can never be mistaken for a generated one.
190192
const compressed = await compressImageContentParts(budgeted.parts, {
193+
maxEdge: options.maxImageEdgePx,
191194
telemetry:
192195
options.telemetry === undefined
193196
? undefined

packages/agent-core/src/rpc/core-impl.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { PluginManager } from '#/plugin';
77
import { LocalFetchURLProvider } from '#/tools/providers/local-fetch-url';
88
import { MoonshotFetchURLProvider } from '#/tools/providers/moonshot-fetch-url';
99
import { MoonshotWebSearchProvider } from '#/tools/providers/moonshot-web-search';
10-
import { setConfiguredMaxImageEdgePx, setConfiguredReadImageByteBudget } from '#/tools/support/image-compress';
10+
import { ImageLimits } from '#/tools/support/image-limits';
1111
import type { PromisableMethods } from '#/utils/types';
1212
import { getCoreVersion } from '#/version';
1313
import { resolveThinkingEffort } from '../agent/config/thinking';
@@ -169,6 +169,8 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
169169
private pluginsLoadError: Error | undefined;
170170
private readonly appVersion: string | undefined;
171171
private readonly experimentalFlags: FlagResolver;
172+
/** Owner-scoped [image] limits; reload pushes the new config via setConfig. */
173+
readonly imageLimits: ImageLimits;
172174

173175
constructor(
174176
protected readonly rpcClient: CoreRPCClient,
@@ -206,8 +208,7 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
206208
FLAG_DEFINITIONS,
207209
this.config.experimental,
208210
);
209-
setConfiguredMaxImageEdgePx(this.config.image?.maxEdgePx);
210-
setConfiguredReadImageByteBudget(this.config.image?.readByteBudget);
211+
this.imageLimits = new ImageLimits(process.env, this.config.image);
211212
this.sessionStore = new SessionStore(this.homeDir);
212213
this.plugins = new PluginManager({ kimiHomeDir: this.homeDir });
213214
// Capture the error rather than swallow it: mutators and explicit /plugins
@@ -301,6 +302,7 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
301302
skills: this.resolveSessionSkillConfig(config),
302303
mcpConfig,
303304
experimentalFlags: this.experimentalFlags,
305+
imageLimits: this.imageLimits,
304306
telemetry: sessionTelemetry,
305307
pluginSessionStarts,
306308
pluginCommands,
@@ -436,6 +438,7 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
436438
skills: this.resolveSessionSkillConfig(config),
437439
mcpConfig,
438440
experimentalFlags: this.experimentalFlags,
441+
imageLimits: this.imageLimits,
439442
telemetry: withTelemetryContext(this.telemetry, { sessionId: summary.id }),
440443
initializeMainAgent: false,
441444
pluginSessionStarts,
@@ -1082,8 +1085,7 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
10821085
private setRuntimeConfig(config: KimiConfig): KimiConfig {
10831086
this.config = config;
10841087
this.experimentalFlags.setConfigOverrides(config.experimental);
1085-
setConfiguredMaxImageEdgePx(config.image?.maxEdgePx);
1086-
setConfiguredReadImageByteBudget(config.image?.readByteBudget);
1088+
this.imageLimits.setConfig(config.image);
10871089
return this.config;
10881090
}
10891091

packages/agent-core/src/services/coreProcess/coreProcess.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { createDecorator } from '../../di';
3333
import type { CoreRPC, KimiCoreOptions } from '../../rpc';
3434
import type { TelemetryClient } from '../../telemetry';
3535
import { type KimiHostIdentity } from '@moonshot-ai/kimi-code-oauth';
36+
import type { ImageLimits } from '#/tools/support/image-limits';
3637

3738
export interface CoreProcessServiceOptions extends KimiCoreOptions {
3839
/**
@@ -68,6 +69,12 @@ export interface ICoreProcessService {
6869
*/
6970
readonly telemetry?: TelemetryClient | undefined;
7071

72+
/**
73+
* The core's owner-scoped [image] limits, so daemon-side prompt-ingestion
74+
* compression uses the same settings (and reloads) as the core's own tools.
75+
*/
76+
readonly imageLimits?: ImageLimits | undefined;
77+
7178
/**
7279
* Resolves once `KimiCore` is fully constructed and the SDK side of the
7380
* in-process RPC has been bound. Repeated calls return the cached promise.

packages/agent-core/src/services/coreProcess/coreProcessService.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import { createRPC, KimiCore } from '../../rpc';
6+
import type { ImageLimits } from '../../tools/support/image-limits';
67
import { Disposable, registerSingleton, SyncDescriptor } from '../../di';
78
import type { CoreAPI, CoreRPC, SDKAPI } from '../../rpc';
89
import type { OAuthTokenProviderResolver } from '../../session/provider-manager';
@@ -36,6 +37,11 @@ export class CoreProcessService extends Disposable implements ICoreProcessServic
3637

3738
public readonly telemetry: TelemetryClient;
3839

40+
/** The core's owner-scoped [image] limits; see ICoreProcessService. */
41+
public get imageLimits(): ImageLimits {
42+
return this._core.imageLimits;
43+
}
44+
3945
/**
4046
* The in-process `KimiCore` instance. Kept private so daemon-side code can't
4147
* grab it and bypass the peer-service indirection.

packages/agent-core/src/session/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import { SessionSubagentHost } from './subagent-host';
5252
import { sessionMediaOriginalsDir } from '../tools/support/image-originals';
5353
import type { ToolServices } from '../tools/support/services';
5454
import { FlagResolver, type ExperimentalFlagResolver } from '../flags';
55+
import { ImageLimits } from '../tools/support/image-limits';
5556
import { abortError } from '../utils/abort';
5657

5758
export interface SessionOptions {
@@ -75,6 +76,8 @@ export interface SessionOptions {
7576
readonly pluginCommands?: readonly PluginCommandDef[];
7677
readonly appVersion?: string;
7778
readonly experimentalFlags?: ExperimentalFlagResolver;
79+
/** Owner-scoped [image] limits, threaded from the owning core into every agent. */
80+
readonly imageLimits?: ImageLimits;
7881
readonly additionalDirs?: readonly string[];
7982
/**
8083
* Print-mode (`kimi -p`) only: hold the main turn open while background
@@ -169,6 +172,7 @@ export class Session {
169172
private readonly logHandle: SessionLogHandle | undefined;
170173
readonly hookEngine: HookEngine;
171174
readonly experimentalFlags: ExperimentalFlagResolver;
175+
readonly imageLimits: ImageLimits;
172176
private toolKaos: Kaos;
173177
private persistenceKaos: Kaos;
174178
private additionalDirs: readonly string[];
@@ -202,6 +206,7 @@ export class Session {
202206
(options.id === undefined ? log : log.createChild({ sessionId: options.id }));
203207
this.rpc = options.rpc;
204208
this.experimentalFlags = options.experimentalFlags ?? new FlagResolver();
209+
this.imageLimits = options.imageLimits ?? new ImageLimits();
205210
this.hookEngine = new HookEngine(options.hooks, {
206211
cwd: options.kaos.getcwd(),
207212
sessionId: options.id,
@@ -837,6 +842,7 @@ export class Session {
837842
pluginSessionStarts: type === 'main' ? this.options.pluginSessionStarts : undefined,
838843
pluginCommands: type === 'main' ? this.options.pluginCommands : undefined,
839844
experimentalFlags: this.experimentalFlags,
845+
imageLimits: this.imageLimits,
840846
additionalDirs: parentAgent?.getAdditionalDirs() ?? this.additionalDirs,
841847
systemPromptContextProvider: () =>
842848
prepareSystemPromptContext(

packages/agent-core/src/tools/builtin/file/read-media.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ import {
4545
compressImageForModel,
4646
cropImageForModel,
4747
formatByteSize,
48-
resolveReadImageByteBudget,
4948
type ImageCompressionTelemetry,
5049
type ImageCropRegion,
5150
} from '../../support/image-compress';
51+
import { ImageLimits } from '../../support/image-limits';
5252
import { toInputJsonSchema } from '../../support/input-schema';
5353
import { literalRulePattern, matchesPathRuleSubject } from '../../support/rule-match';
5454
import type { WorkspaceConfig } from '../../support/workspace';
@@ -258,12 +258,14 @@ export class ReadMediaFileTool implements BuiltinTool<ReadMediaFileInput> {
258258
readonly description: string;
259259
readonly parameters: Record<string, unknown> = toInputJsonSchema(ReadMediaFileInputSchema);
260260
private readonly compressTelemetry: ImageCompressionTelemetry | undefined;
261+
private readonly imageLimits: ImageLimits;
261262
constructor(
262263
private readonly kaos: Kaos,
263264
private readonly workspace: WorkspaceConfig,
264265
private readonly capabilities: ModelCapability,
265266
private readonly videoUploader?: VideoUploader | undefined,
266267
telemetry?: TelemetryClient,
268+
imageLimits?: ImageLimits,
267269
) {
268270
if (!capabilities.image_in && !capabilities.video_in) {
269271
const skip = new Error('ReadMediaFile requires image_in or video_in capability');
@@ -273,6 +275,7 @@ export class ReadMediaFileTool implements BuiltinTool<ReadMediaFileInput> {
273275
this.description = buildDescription(capabilities);
274276
this.compressTelemetry =
275277
telemetry === undefined ? undefined : { client: telemetry, source: 'read_media' };
278+
this.imageLimits = imageLimits ?? new ImageLimits();
276279
}
277280

278281
resolveExecution(args: ReadMediaFileInput): ToolExecution {
@@ -387,6 +390,7 @@ export class ReadMediaFileTool implements BuiltinTool<ReadMediaFileInput> {
387390
// full fidelity, so a prior downsampled view can be zoomed into.
388391
const outcome = await cropImageForModel(data, fileType.mimeType, args.region, {
389392
skipResize: args.full_resolution === true,
393+
maxEdge: this.imageLimits.maxEdgePx(),
390394
telemetry: this.compressTelemetry,
391395
});
392396
if (!outcome.ok) {
@@ -446,7 +450,8 @@ export class ReadMediaFileTool implements BuiltinTool<ReadMediaFileInput> {
446450
// failure compressImageForModel returns the original bytes, so the
447451
// read still succeeds with the uncompressed image.
448452
const compressed = await compressImageForModel(data, fileType.mimeType, {
449-
byteBudget: resolveReadImageByteBudget(),
453+
maxEdge: this.imageLimits.maxEdgePx(),
454+
byteBudget: this.imageLimits.readByteBudget(),
450455
telemetry: this.compressTelemetry,
451456
});
452457
const base64 = Buffer.from(compressed.data).toString('base64');

0 commit comments

Comments
 (0)