Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/tidy-media-tool-hints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Stop pointing models without image or video input capability to the unavailable ReadMediaFile tool.
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/agent/tools/os/read/read.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ When you need several files, prefer to read them in parallel: emit multiple `Rea
- Returns up to ${MAX_LINES} lines or ${MAX_BYTES_KB} KB per call, whichever comes first; lines longer than ${MAX_LINE_LENGTH} chars are truncated mid-line.
- Page larger files with `line_offset` (1-based start line) and `n_lines`. Omit `n_lines` to read up to the ${MAX_LINES}-line cap.
- Sensitive files (`.env` files, credential stores, SSH private keys, and similar secrets) are refused to protect secrets; do not attempt to read them. Templates and public keys are exempt: `.env.example` / `.env.sample` / `.env.template` and public SSH keys such as `id_rsa.pub` read normally.
- UTF-8 text files are read directly. UTF-16 LE/BE text files (with or without a BOM) are detected automatically and transcoded to UTF-8 for display; the status block notes the detected encoding, and Edit/Write on such a file still expect UTF-8 — convert its encoding first (e.g. `iconv` via Bash). Other encodings (e.g. GBK), binary files, and files containing NUL bytes are refused; use `ReadMediaFile` for images or video, and Bash or an MCP tool for other binary formats.
- UTF-8 text files are read directly. UTF-16 LE/BE text files (with or without a BOM) are detected automatically and transcoded to UTF-8 for display; the status block notes the detected encoding, and Edit/Write on such a file still expect UTF-8 — convert its encoding first (e.g. `iconv` via Bash). Other encodings (e.g. GBK), binary files, and files containing NUL bytes are refused; ${BINARY_FORMATS_HINT}
- Negative line_offset reads from the end of the file (for example, -100 reads the last 100 lines); the absolute value cannot exceed ${MAX_LINES}.
- Output format: `<line-number>\t<content>` per line.
- A `<system>...</system>` status block is appended after the file content; it summarizes how much was read (line and byte counts, truncation, line-ending notes) and is not part of the file itself.
Expand Down
75 changes: 59 additions & 16 deletions packages/agent-core-v2/src/agent/tools/os/read/readTool.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { IHostFileSystem } from '#/os/interface/hostFileSystem';
import { IAgentRuntimeService, inspectAgentRuntime } from '#/agent/runtimeBinding/agentRuntime';
import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry';
import { IAgentToolPolicyService } from '#/agent/toolPolicy/toolPolicy';
import { RuntimeWorkspaceView } from '#/runtime/runtimeWorkspaceView';
import { unwrapErrorCause } from '#/_base/errors/errors';
import { ISessionSkillCatalog } from '#/session/sessionSkillCatalog/skillCatalog';
Expand Down Expand Up @@ -180,12 +182,16 @@ async function* decodedLines(lines: readonly string[]): AsyncGenerator<string> {
yield* lines;
}

function notReadableFileOutput(path: string): string {
return (
`"${path}" is not readable as UTF-8 text. ` +
'If it is an image or video, use ReadMediaFile. ' +
'For other binary formats, use Bash or an MCP tool if available.'
);
function notReadableFileOutput(path: string, mediaReadable: boolean, bashAvailable: boolean): string {
const binary = bashAvailable ? 'use Bash or an MCP tool if available.' : 'use an available MCP tool.';
if (mediaReadable) {
return (
`"${path}" is not readable as UTF-8 text. ` +
'If it is an image or video, use ReadMediaFile. ' +
`For other binary formats, ${binary}`
);
}
return `"${path}" is not readable as UTF-8 text. For binary formats, ${binary}`;
}

function notUtf8DecodableFileOutput(path: string): string {
Expand All @@ -196,23 +202,58 @@ function notUtf8DecodableFileOutput(path: string): string {
);
}

const READ_DESCRIPTION = renderPrompt(readDescriptionTemplate, {
MAX_LINES,
MAX_BYTES_KB: MAX_BYTES / 1024,
MAX_LINE_LENGTH,
});
function binaryFormatsHint(mediaReadable: boolean, bashAvailable: boolean): string {
const binary = bashAvailable ? 'Bash or an MCP tool' : 'an available MCP tool';
return mediaReadable
? `use \`ReadMediaFile\` for images or video, and ${binary} for other binary formats.`
: `use ${binary} for binary formats.`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid recommending MCP when no MCP tool is active

When Bash is unavailable, this still tells the model to use an MCP tool without checking whether the registry contains any policy-active MCP tools. Fresh evidence in this revision is the new fallback itself: the plan profile at packages/agent-core-v2/src/features/plan/profile/plan.ts:8-15 allows neither Bash nor mcp__*, so a text-only plan agent receives guidance to use a tool it cannot call. Inspect active MCP registry entries before mentioning MCP, or report that no binary-reading fallback is available.

Useful? React with 👍 / 👎.

}

export class ReadTool implements IReadTool {
declare readonly _serviceBrand: undefined;
readonly name = 'Read' as const;
readonly description = READ_DESCRIPTION;
readonly parameters: Record<string, unknown> = toInputJsonSchema(ReadInputSchema);
constructor(
@IAgentRuntimeService private readonly runtime: IAgentRuntimeService,
@ISessionWorkspaceContext private readonly workspaceCtx: ISessionWorkspaceContext,
@ISessionSkillCatalog private readonly skillCatalog: ISessionSkillCatalog,
@IAgentToolRegistryService private readonly toolRegistry: IAgentToolRegistryService,
@IAgentToolPolicyService private readonly toolPolicy: IAgentToolPolicyService,
) {}

get description(): string {
return renderPrompt(readDescriptionTemplate, {
MAX_LINES,
MAX_BYTES_KB: MAX_BYTES / 1024,
MAX_LINE_LENGTH,
BINARY_FORMATS_HINT: binaryFormatsHint(this.mediaReadable(), this.bashAvailable()),
});
}

private mediaReadable(): boolean {
return this.mediaRegistered() && this.toolPolicy.isToolActive('ReadMediaFile');
}

private mediaRegistered(): boolean {
return this.toolRegistry.resolve('ReadMediaFile') !== undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor the effective tool policy before advertising media reads

When a multimodal model registers ReadMediaFile but the workspace, profile, global, or session policy disables it, AgentToolSelectService.activeEntries() removes the tool from the model request and the executor guard rejects calls, while this registry-only check still returns true. Consequently, the new description and rejection messages continue directing the model to an unavailable tool in exactly those denylist scenarios; determine availability through the effective IAgentToolPolicyService as well as registry presence.

AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L7-L7

Useful? React with 👍 / 👎.

}

private bashAvailable(): boolean {
return (
this.toolRegistry.resolve('Bash') !== undefined && this.toolPolicy.isToolActive('Bash')
);
}

private mediaUnavailableOutput(path: string, kind: string): string {
const reason = this.mediaRegistered()
? 'The ReadMediaFile tool is disabled by the active tool policy, so image and video reading is unavailable.'
: 'Image and video reading is unavailable for this agent.';
const fallback = this.bashAvailable()
? ' Use Bash or an MCP tool to inspect or convert it.'
: ' Use an available MCP tool to inspect or convert it.';
return `"${path}" is a ${kind} file. ${reason}${fallback}`;
}

private workspaceConfig(view: RuntimeWorkspaceView): WorkspaceConfig {
return { workspaceDir: view.workDir, additionalDirs: view.additionalDirs };
}
Expand Down Expand Up @@ -275,7 +316,9 @@ export class ReadTool implements IReadTool {
if (fileType.kind === 'image' || fileType.kind === 'video') {
return {
isError: true,
output: `"${args.path}" is a ${fileType.kind} file. Use ReadMediaFile to read image or video files.`,
output: this.mediaReadable()
? `"${args.path}" is a ${fileType.kind} file. Use ReadMediaFile to read image or video files.`
: this.mediaUnavailableOutput(args.path, fileType.kind),
};
}

Expand All @@ -298,7 +341,7 @@ export class ReadTool implements IReadTool {
} else if (fileType.kind === 'unknown') {
return {
isError: true,
output: notReadableFileOutput(args.path),
output: notReadableFileOutput(args.path, this.mediaReadable(), this.bashAvailable()),
};
} else {
lines = fs.readLines(safePath, { errors: 'strict' });
Expand Down Expand Up @@ -353,7 +396,7 @@ export class ReadTool implements IReadTool {

for await (const rawLine of lines) {
if (containsNulByte(rawLine)) {
return { isError: true, output: notReadableFileOutput(displayPath) };
return { isError: true, output: notReadableFileOutput(displayPath, this.mediaReadable(), this.bashAvailable()) };
}
currentLineNo += 1;
updateLineEndingFlags(flags, rawLine);
Expand Down Expand Up @@ -411,7 +454,7 @@ export class ReadTool implements IReadTool {

for await (const rawLine of lines) {
if (containsNulByte(rawLine)) {
return { isError: true, output: notReadableFileOutput(displayPath) };
return { isError: true, output: notReadableFileOutput(displayPath, this.mediaReadable(), this.bashAvailable()) };
}
currentLineNo += 1;
updateLineEndingFlags(flags, rawLine);
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/features/plan/profile/plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const PLAN_ROLE =
'1. What you already know from the information provided\n' +
'2. What questions remain unanswered that would benefit from explore agent investigation\n' +
'3. Your implementation plan (either preliminary if questions remain, or final if sufficient context exists)\n\n' +
'You are a read-only planning agent: you can read and search files (Read, Glob, Grep, ReadMediaFile) ' +
'You are a read-only planning agent: you can read and search files (Read, Glob, Grep) ' +
'and consult the web (WebSearch, FetchURL), but you have no shell and no file-editing tools. ' +
'Where the general instructions tell you to make changes with tools, that does not apply to you — ' +
'do not attempt to run commands or modify files. Your deliverable is the plan itself, returned as ' +
Expand Down
4 changes: 2 additions & 2 deletions packages/agent-core-v2/test/agent/loop/loop.test.ts

Large diffs are not rendered by default.

Loading