Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
47341e3
feat(agent-core): strengthen default system prompt
RealKai42 Jun 24, 2026
e775898
feat(agent-core): hoist key working rules into the system prompt
RealKai42 Jun 24, 2026
c3d5634
fix: clarify guidelines for file pattern matching and tool usage in e…
RealKai42 Jun 24, 2026
76a7258
fix(agent-core): hide the Skills section from agents without the Skil…
RealKai42 Jun 24, 2026
779d4fe
Merge remote-tracking branch 'origin/main' into kaiyi/madison
RealKai42 Jun 24, 2026
88221b4
fix(agent-core): gate Agent, background-task, and TodoList guidance b…
RealKai42 Jun 25, 2026
3deba36
refactor(agent-core): move Agent-delegation and Glob-anchor guidance …
RealKai42 Jun 25, 2026
33bed31
test(agent-core): add guidance for blast-radius and concrete examples…
RealKai42 Jun 25, 2026
4ebe763
Merge remote-tracking branch 'origin/main' into kaiyi/madison
RealKai42 Jun 25, 2026
e7ae4b1
docs: update descriptions for skill-tool and fetch-url; enhance web-s…
RealKai42 Jun 25, 2026
07cc7b4
feat(agent-core): disclose enforced constraints in tool descriptions;…
RealKai42 Jun 25, 2026
2822e32
fix(agent-core): soften AskUserQuestion answers-keying wording to mat…
RealKai42 Jun 25, 2026
71623d0
feat(agent-core): tighten Bash/Grep/Write/Edit tool descriptions
RealKai42 Jun 25, 2026
7e7a9ba
feat(agent-core): refine plan-mode/todo/cron tool descriptions
RealKai42 Jun 25, 2026
d881b5c
refactor(agent-core): dedupe ExitPlanMode options docs into the param…
RealKai42 Jun 25, 2026
9e843f5
Merge remote-tracking branch 'origin/main' into kaiyi/madison
RealKai42 Jun 25, 2026
c84917d
fix(agent-core): correct prompt/code inaccuracies found in the final …
RealKai42 Jun 25, 2026
469625b
fix(agent-core): gate prompt tool guidance on runtime availability, n…
RealKai42 Jun 25, 2026
def27e6
fix(agent-core): exempt the plan-mode plan file from the Write *.md ban
RealKai42 Jun 25, 2026
d65132a
fix(agent-core): scope plan-mode prompt guidance and the Write *.md b…
RealKai42 Jun 25, 2026
8ebfd75
refactor(agent-core): move tool-coupled guidance into tool descriptions
RealKai42 Jun 25, 2026
c76c302
fix(skill-tool): clarify no-reinvoke guard and argument handling in t…
RealKai42 Jun 25, 2026
ea032f7
feat(fetch-url): indicate content retrieval mode in output for better…
RealKai42 Jun 25, 2026
9ccf56c
fix(agent-core): correct goal-budget rounding and task-output failure…
RealKai42 Jun 25, 2026
163ca3e
fix(agent-core): scope free-work guidance by role and steer one-shots…
RealKai42 Jun 26, 2026
410e492
fix(enter-plan-mode): clarify availability of Agent tool in plan mode…
RealKai42 Jun 26, 2026
7f344b6
fix(agent-core): surface Grep count_matches total and pagination in o…
RealKai42 Jun 26, 2026
2cd1767
fix(grep): reorder count summary and results in output for clarity
RealKai42 Jun 26, 2026
76676fd
Merge remote-tracking branch 'origin/main' into kaiyi/madison
RealKai42 Jun 26, 2026
9e368de
chore(changeset): consolidate prompt-hardening changesets into one
RealKai42 Jun 26, 2026
3f5478a
fix(agent-core): stop the agent from blocking on background tasks
RealKai42 Jun 26, 2026
de61971
Merge branch 'main' into kaiyi/madison
RealKai42 Jun 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/harden-system-prompt-and-tool-descriptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Harden the default system prompt and built-in tool descriptions: stop the agent from blocking on background tasks it should let run, keep its guidance matched to the tools each profile actually provides, and surface tool-result details (fetched-page mode, Grep match totals) it previously missed.
2 changes: 1 addition & 1 deletion packages/agent-core/src/agent/injection/plan-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ function fullReminder(planFilePath: PlanFilePath): string {
return inlineFullReminder();
}

const body = `Plan mode is active. You MUST NOT make any edits (with the exception of the current plan file) or otherwise make changes to the system unless a tool request is explicitly approved. Prefer read-only tools. Use Bash only when needed; Bash follows the normal permission mode and rules. This supersedes any other instructions you have received.
const body = `Plan mode is active. You MUST NOT make any edits (with the exception of the current plan file) or otherwise make changes to the system unless a tool request is explicitly approved. Prefer read-only tools. Use Bash only when needed; Bash follows the normal permission mode and rules. This supersedes any other instructions you have received. TaskStop, CronCreate, and CronDelete are also blocked in plan mode — call ExitPlanMode first if you need them.

Workflow:
1. Understand — explore the codebase with Glob, Grep, Read.
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core/src/profile/default/explore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ promptVars:
- Running read-only shell commands (git log, git diff, ls, find, etc.)

Guidelines:
- Use Glob for broad file pattern matching. Patterns MUST contain a literal anchor (extension or subdirectory); pure wildcards like `*` or `**/*` are rejected by the tool.
- Use Glob for broad file pattern matching. Prefer a pattern with a literal anchor (extension or subdirectory): results are capped at a fixed number of matches, so a broad pattern like `**/*` can truncate before reaching what you need.
- Use Grep for searching file contents with regex
- Use Read when you know the specific file path
- Use Bash ONLY for read-only operations (ls, git status, git log, git diff, find)
Expand Down
79 changes: 35 additions & 44 deletions packages/agent-core/src/profile/default/system.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions packages/agent-core/src/profile/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ function toResolvedProfile(merged: MergedAgentProfile): ResolvedAgentProfile {
*/
function createSystemPromptRenderer(merged: MergedAgentProfile): SystemPromptRenderer {
return (context: SystemPromptContext): string => {
const vars = buildTemplateVars(context, merged.promptVars);
const vars = buildTemplateVars(context, merged.promptVars, merged.tools);
try {
return renderPrompt(merged.systemPromptTemplate, vars);
} catch (error) {
Expand All @@ -140,6 +140,7 @@ function createSystemPromptRenderer(merged: MergedAgentProfile): SystemPromptRen
function buildTemplateVars(
context: SystemPromptContext,
promptVars: Record<string, string>,
tools: readonly string[],
): Record<string, string> {
const skills =
typeof context.skills === 'string'
Expand All @@ -158,7 +159,7 @@ function buildTemplateVars(
KIMI_WORK_DIR: context.cwd,
KIMI_WORK_DIR_LS: context.cwdListing ?? '',
KIMI_AGENTS_MD: context.agentsMd ?? '',
KIMI_SKILLS: skills,
KIMI_SKILLS: tools.includes('Skill') ? skills : '',
KIMI_ADDITIONAL_DIRS_INFO: context.additionalDirsInfo ?? '',
ROLE_ADDITIONAL:
context.roleAdditional ?? promptVars['ROLE_ADDITIONAL'] ?? promptVars['roleAdditional'] ?? '',
Expand Down
5 changes: 3 additions & 2 deletions packages/agent-core/src/tools/background/task-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ List background tasks and their current status.

Use this tool to discover which background tasks exist and where each one
stands. It is the entry point for inspecting background work: it returns a
task ID, status, command, description, and PID for every task it reports,
plus the exit code and stop reason for tasks that have already finished.
task ID, status, and description for every task it reports, plus the command,
PID, and (once finished) exit code for shell tasks, and a stop reason for any
task that ended early.

Guidelines:

Expand Down
3 changes: 2 additions & 1 deletion packages/agent-core/src/tools/background/task-output.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ Use this after `Bash(run_in_background=true)` or `Agent(run_in_background=true)`

Guidelines:
- Prefer relying on automatic completion notifications. Use this tool only when you need task output before the automatic notification arrives.
- Do not use TaskOutput to wait for a result you need before continuing — if your next step depends on the task's result, run that task in the foreground instead. TaskOutput is for a deliberate progress check you will act on without blocking, not a way to sit and wait for a background task you just launched.
- By default this tool is non-blocking and returns a current status/output snapshot.
- Use block=true only when you intentionally want to wait for completion or timeout.
- This tool returns structured task metadata, a fixed-size output preview, and an output_path for the full log.
- For a terminal task, the metadata also explains why it ended: `status: timed_out` when a task was aborted by its deadline, and `stop_reason` when the task was explicitly stopped. `terminal_reason` is a categorical label for the same event — its value is `timed_out` or `stopped` — and is emitted alongside the matching status / `stop_reason` field. A task that ended on its own emits neither `stop_reason` nor `terminal_reason`.
- For a terminal task, the metadata also explains why it ended. A shell command that runs to completion reports `status: completed` on a zero exit, or `status: failed` with its non-zero `exit_code` — judge that failure from the `exit_code`, because a plain command failure carries no `stop_reason` and no `terminal_reason`. `terminal_reason` is a categorical label emitted only when the end is not an ordinary exit: `timed_out` when the deadline aborted it, `stopped` when it was explicitly stopped, or `failed` when it errored without producing an exit code; the `stopped` and `failed` cases also carry a human-readable `stop_reason`. A task that finished on its own with a clean exit carries neither `stop_reason` nor `terminal_reason`.
- The full, never-truncated log is always available at output_path; use the `Read` tool with that path to page through it, whether or not the preview was truncated.
- This tool works with the generic background task system and should remain the primary read path for future task types, not just bash.
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
When `run_in_background=true`, the subagent runs detached from this turn. The completion arrives in a later turn as a synthetic user-role message containing its result — you do not need to poll, sleep, or check on its progress. Continue with other work or respond to the user. Never fabricate or predict what the result will say.

Default to a foreground subagent (omit `run_in_background`) when your next step needs its result — foreground hands the result straight back. Reach for `run_in_background=true` only when you have other work to do while it runs and do not need its result to proceed. Never launch in the background and then immediately wait on it (with `TaskOutput` or otherwise): that just blocks the turn for no benefit — run it in the foreground instead.
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
Launch multiple subagents from one prompt template, existing agent resumes, or both.

Use AgentSwarm when many subagents should run the same kind of task over different inputs. The placeholder is exactly `{{item}}`. For example, with `prompt_template` set to `Review {{item}} for likely regressions.` and `items` set to `["src/a.ts", "src/b.ts"]`, AgentSwarm launches two new subagents with those two concrete prompts.
Use AgentSwarm when many subagents should run the same kind of task over different inputs. The placeholder is exactly `{{item}}`. For example, with `prompt_template` set to `Review {{item}} for likely regressions.` and `items` set to `["src/a.ts", "src/b.ts"]`, AgentSwarm launches two new subagents with those two concrete prompts. For a few differently-shaped tasks, make separate `Agent` calls in one message instead.

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 Qualify Agent fallback in AgentSwarm guidance

Fresh evidence beyond the earlier Agent-gating findings is this newly added AgentSwarm description: in a custom profile that exposes AgentSwarm but trims Agent, the model still receives this tool description while the Agent tool is absent from the schema, so the recommended fallback for differently-shaped tasks points it at a call it cannot make. Qualify this fallback with “if Agent is available” or describe an AgentSwarm-only alternative.

Useful? React with 👍 / 👎.


Use `resume_agent_ids` to continue subagents that already exist from earlier work, such as ones that failed or timed out: map each agent id to the prompt for that resumed subagent (usually `continue` if no extra information is needed). You may combine `resume_agent_ids` with `items` in the same call to resume existing subagents and launch new ones. Do not duplicate resumed work in `items`.

Each of these is enforced — a violation is rejected before any subagent starts: provide at least 2 `items` unless you pass `resume_agent_ids`; whenever `items` are present, `prompt_template` is required and must contain `{{item}}`; and the filled-in prompts must be distinct (two items that expand to the same prompt are rejected).

Use enough subagents to keep the work focused and parallel. AgentSwarm supports up to 128 subagents, and launches are queued automatically, so it is safe to split large tasks into many clear, independent items.

If `AgentSwarm` is called, that call must be the only tool call in the response.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Launch a subagent to handle a task. The subagent runs as a same-process loop instance with its own context and wire file.
Launch a subagent to handle a task. The subagent runs as a same-process loop instance with its own context and wire file. Delegating also keeps the bulk of intermediate file contents out of your own context — you get a conclusion back instead of a pile of dumps.

Writing the prompt:
- The subagent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.
Expand Down
6 changes: 4 additions & 2 deletions packages/agent-core/src/tools/builtin/collaboration/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ export const AgentToolInputSchema = z.preprocess(
resume: z
.string()
.optional()
.describe('Optional agent ID to resume instead of creating a new instance'),
.describe(
'Optional agent ID to resume instead of creating a new instance. When set, do not also pass subagent_type — the resumed agent keeps its own type, and supplying both is rejected.',
),
run_in_background: z
.boolean()
.optional()
Expand Down Expand Up @@ -323,7 +325,7 @@ function formatBackgroundAgentResult(
`description: ${description}`,
'',
allowBackground
? `next_step: The completion arrives automatically in a later turn — no polling needed. To peek at progress without blocking, call TaskOutput(task_id="${taskId}", block=false).`
? `next_step: The completion arrives automatically in a later turn — do NOT wait, poll, or call TaskOutput on it; continue with other work or hand back to the user. (If you have nothing to do until it finishes, run such tasks in the foreground next time.)`
: 'next_step: The completion arrives automatically in a later turn.',
`resume_hint: To continue or recover this same subagent later, call Agent(resume="${handle.agentId}", prompt="..."). The parameter is agent_id ("${handle.agentId}"), NOT task_id ("${taskId}") or source_id from a later <notification>. Recovery cases: a later <notification type="task.lost" | "task.failed" | "task.killed"> for this subagent — its conversation history is preserved across session restarts and resume will pick it up.`,
].join('\n');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ Overusing this tool interrupts the user's flow. Only use it when the user's inpu
- Each question should have 2-4 meaningful, distinct options
- You can ask 1-4 questions at a time; group related questions to minimize interruptions
- If you recommend a specific option, list it first and append "(Recommended)" to its label
- The result is JSON with an `answers` object whose keys identify each answered question; if `answers` is empty and a `note` says the user dismissed it, they declined to answer — proceed with your best judgment and do not re-ask the same question
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Invoke a registered skill from the current skill listing. BLOCKING REQUIREMENT: when a skill from the listing matches the user's request, you MUST call this tool (not free-form text). Do NOT call the same skill repeatedly inside one turn — recursive depth is capped at {{ MAX_SKILL_QUERY_DEPTH }}.
Invoke a registered skill from the current skill listing. BLOCKING REQUIREMENT: when a skill from the listing matches the user's request, you MUST call this tool (not free-form text). Do not re-invoke a skill to repeat work already done: if a `<kimi-skill-loaded>` block for it with the same `args` is already present in the conversation, follow those instructions directly instead of calling the tool again. Do call the tool again when you need the skill with different arguments — the loaded block was expanded with the earlier `args` and will not reflect new inputs.
17 changes: 12 additions & 5 deletions packages/agent-core/src/tools/builtin/collaboration/skill-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,17 @@ export interface SkillToolInput {
}

export const SkillToolInputSchema: z.ZodType<SkillToolInput> = z.object({
skill: z.string(),
args: z.string().optional(),
skill: z
.string()
.describe(
'The exact name of the skill to invoke, spelled as it appears in the current skill listing (e.g. "commit", "pdf").',
),
args: z
.string()
.optional()
.describe(
'Optional argument string for the skill, written like a command line (e.g. `-m "fix bug"`, `123`, a file path). It is split on whitespace, with quotes grouping a token, and expanded into the skill\'s declared placeholders. Omit it for skills that take no arguments.',
),
});

export interface SkillToolOptions {
Expand All @@ -67,9 +76,7 @@ export interface SkillToolOptions {

export class SkillTool implements BuiltinTool<SkillToolInput> {
readonly name = 'Skill';
readonly description: string = renderPrompt(skillDescriptionTemplate, {
MAX_SKILL_QUERY_DEPTH,
});
readonly description: string = renderPrompt(skillDescriptionTemplate, {});
readonly parameters: Record<string, unknown> = toInputJsonSchema(SkillToolInputSchema);

constructor(
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core/src/tools/builtin/file/edit.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Perform exact replacements in existing files.
- Take `old_string` and `new_string` from the Read output view.
- Drop the line-number prefix and tab; match only file content.
- `old_string` must be unique unless `replace_all` is set.
- If `old_string` is ambiguous, add surrounding context. Use `replace_all` only when every occurrence should change.
- If `old_string` is ambiguous, add surrounding context. Use `replace_all` only when every occurrence should change — for example, renaming a symbol throughout the file.
- Multiple Edit calls may run in one response only when they do not target the same file.
- DO NOT issue consecutive Edit calls on the same file. A previous Edit can invalidate a later Edit's `old_string`, causing `old_string not found`. Read the file again before the next Edit.
- A write lock serializes same-file edits in response order, but serialization does not make stale `old_string` valid.
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core/src/tools/builtin/file/glob.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Good patterns:
- `*.{ts,tsx}` — brace expansion is supported; expanded into `*.ts` and `*.tsx` before walking
- `{src,test}/**/*.ts` — cartesian brace expansion is supported too

Results are capped at the first 100 matching paths (walk order, not global modification-time order). If a search would return more, a truncation marker is appended with the count of matches seen so far. Refine the pattern (extension, subdirectory) when 100 is not enough, or call again with a narrower anchor.
Prefer a pattern with a literal anchor (a file extension or subdirectory) up front; a bare `**/*` walks until it truncates at the match cap. Results are capped at the first 100 matching paths (walk order, not global modification-time order). If a search would return more, a truncation marker is appended with the count of matches seen so far. Refine the pattern (extension, subdirectory) when 100 is not enough, or call again with a narrower anchor.

Large-directory caveat — avoid recursing into dependency / build output even with an anchor:
- `node_modules/**/*.js`, `.venv/**/*.py`, `__pycache__/**`, `target/**` all match technically but
Expand Down
32 changes: 15 additions & 17 deletions packages/agent-core/src/tools/builtin/file/grep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const GrepInputSchema = z.object({
.enum(['content', 'files_with_matches', 'count_matches'])
.optional()
.describe(
'Shape of the result. `content` shows matching lines (honors `-A`, `-B`, `-C`, `-n`, and `head_limit`); `files_with_matches` shows only the paths of files that contain a match (honors `head_limit`); `count_matches` shows the total number of matches. Defaults to `files_with_matches`.',
'Shape of the result. `content` shows matching lines (honors `-A`, `-B`, `-C`, `-n`, and `head_limit`); `files_with_matches` shows only the paths of files that contain a match, most-recently-modified first (honors `head_limit`); `count_matches` shows per-file match counts as `path:count` lines, preceded by an aggregate total line. Defaults to `files_with_matches`.',
),
'-i': z.boolean().optional().describe('Perform a case-insensitive search. Defaults to false.'),
'-n': z
Expand Down Expand Up @@ -290,13 +290,14 @@ export class GrepTool implements BuiltinTool<GrepInput> {
const limited = limitActive ? afterOffset.slice(0, headLimit) : afterOffset;
const paginationTruncated = limitActive && afterOffset.length > headLimit;

// Human-readable annotations are appended after visible matches.
// In count mode, the data stream must stay pure `path:count` lines
// — the count summary and pagination notice move to a side channel
// (returned via `result.message`) so they don't contaminate it.
// Other modes keep these notices inline in `output`.
// Notices ride in `output` (not `result.message`, which is dropped before the
// result reaches the model). The count-mode aggregate — the total and the
// "use offset=N to see more" cue — leads the output as a HEADER, written before
// the rows, so ToolResultBuilder's char cap can only ever truncate the rows, not
// the total (count rows are unbounded with head_limit: 0). Incidental notices
// trail the body.
const headerLines: string[] = [];
const messages: string[] = [];
const sideChannelMessages: string[] = [];
if (filteredSensitive.size > 0) {
const displayedFilteredPaths = [...filteredSensitive].map((path) =>
relativizeIfUnder(path, this.workspace.workspaceDir, pathClass),
Expand All @@ -306,14 +307,14 @@ export class GrepTool implements BuiltinTool<GrepInput> {
);
}
if (mode === 'count_matches' && orderedLines.length > 0) {
sideChannelMessages.push(formatCountSummary(orderedLines, filteredSensitive.size > 0));
headerLines.push(formatCountSummary(orderedLines, filteredSensitive.size > 0));
}
if (paginationTruncated) {
const total = afterOffset.length + offset;
const nextOffset = offset + headLimit;
const paginationNotice = `Results truncated to ${String(headLimit)} lines (total: ${String(total)}). Use offset=${String(nextOffset)} to see more.`;
if (mode === 'count_matches') {
sideChannelMessages.push(paginationNotice);
headerLines.push(paginationNotice);
} else {
messages.push(paginationNotice);
}
Expand Down Expand Up @@ -346,18 +347,15 @@ export class GrepTool implements BuiltinTool<GrepInput> {
: contentBody;
const emptyResultMessage =
SENSITIVE_GLOBS_TO_EXCLUDE.length > 0 ? 'No non-sensitive matches found' : 'No matches found';
const combined =
visibleBody === '' && messages.length === 0
const body =
visibleBody === '' && headerLines.length === 0 && messages.length === 0
? emptyResultMessage
: messages.length > 0
? visibleBody === ''
? messages.join('\n')
: `${visibleBody}\n${messages.join('\n')}`
: visibleBody;
: visibleBody;
const combined = [...headerLines, body, ...messages].filter((part) => part !== '').join('\n');

const builder = new ToolResultBuilder();
builder.write(combined);
return builder.ok(sideChannelMessages.join('\n'));
return builder.ok();
}

}
Expand Down
Loading
Loading