Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
6590480
feat: support custom agent files
7Sageer Jul 15, 2026
6262b42
chore: shorten custom agents changeset
7Sageer Jul 15, 2026
d7344f6
fix(agent-core-v2): stringify caught errors in agent catalog log calls
7Sageer Jul 15, 2026
b919c3d
docs: drop v2 engine notes from custom agents docs
7Sageer Jul 15, 2026
6c16401
fix: align custom agent binding semantics across engine and edges
7Sageer Jul 15, 2026
264c8fe
fix: tighten custom agent behavior
7Sageer Jul 15, 2026
6fd4930
fix: address custom agent review findings
7Sageer Jul 15, 2026
2de39ac
fix(agent-core,agent-core-v2): keep active-tool wire records replayab…
7Sageer Jul 15, 2026
b77be0f
fix(agent-core-v2): tolerate unreadable directories in agent-file dis…
7Sageer Jul 15, 2026
6dee92a
fix(agent-core-v2): warn when a same-name builtin suppresses a file p…
7Sageer Jul 15, 2026
3e7f732
refactor(agent-core-v2): pass skillActive explicitly to renderSystemP…
7Sageer Jul 15, 2026
1d43502
refactor(agent-core-v2): fall back to the configured default model in…
7Sageer Jul 15, 2026
adb227c
fix(agent-core-v2,kap-server): reject unsupported thinking atomically…
7Sageer Jul 15, 2026
5cf2c75
fix(agent-core-v2): await the agent profile catalog at session materi…
7Sageer Jul 15, 2026
0714c99
test: cover the --agent-file fatal path and tidy profile registration…
7Sageer Jul 15, 2026
8e46ab7
docs: align custom agent docs with v2-engine gating
7Sageer Jul 15, 2026
8cbc500
test(agent-core-v2): revert unrelated style churn in fs/workspace tests
7Sageer Jul 15, 2026
f9c44d4
Merge remote-tracking branch 'upstream/main' into feat-custom-agent-file
7Sageer Jul 16, 2026
0561dee
Merge remote-tracking branch 'upstream/main' into feat-custom-agent-file
7Sageer Jul 16, 2026
61183b5
feat(agent-core-v2): add permanent system prompt override via SYSTEM.md
7Sageer Jul 17, 2026
f6251cd
feat(agent-core-v2): gate tools globally and accept session disabledT…
7Sageer Jul 17, 2026
637dee4
fix: enforce session tool policy across agents
7Sageer Jul 17, 2026
7ac9e73
fix(agent-core-v2): enforce tool policy at execution
7Sageer Jul 17, 2026
26518f0
fix(agent-core-v2): align subagent tool descriptions with policy
7Sageer Jul 17, 2026
ce5d2e3
fix(agent-core-v2): harden custom agent policy state
7Sageer Jul 17, 2026
2c10985
fix(agent-core-v2): harden custom agent lifecycle
7Sageer Jul 17, 2026
d56f5b7
refactor(agent-core-v2): persist profile binding in a single profile.…
7Sageer Jul 20, 2026
4f1cdba
fix(agent-core-v2): skip unreadable paths during agent file discovery
7Sageer Jul 20, 2026
1ef37c7
fix(agent-core-v2): exempt select_tools from the executor policy guard
7Sageer Jul 20, 2026
f33d7ca
test(agent-core-v2): resolve profile and tool-policy SUTs by interface
7Sageer Jul 20, 2026
043eac2
fix(agent-core-v2): enforce explicit select_tools policy
7Sageer Jul 20, 2026
f7b1691
feat(agent-core-v2): accept Claude-style tool lists and rename agent-…
7Sageer Jul 20, 2026
1bf4daa
feat(agent-core-v2): unify prompt templating on ${var}
7Sageer Jul 20, 2026
5e542c4
docs: complete agent priority chain and link SYSTEM.md precedence
7Sageer Jul 20, 2026
5be864e
test: fix invalid custom agent fixture
7Sageer Jul 20, 2026
7f677c7
fix(cli): reject multiple agent selectors
7Sageer Jul 20, 2026
643fbef
feat(agent-core-v2): add subagents allowlist to agent files
7Sageer Jul 20, 2026
ce7b7d2
fix(agent-core-v2): persist the subagent allowlist in the profile bin…
7Sageer Jul 20, 2026
ebaca04
feat(agent-core-v2): warn on tool patterns that never match
7Sageer Jul 20, 2026
3ea7c51
docs: align --agent-file docs with the single-selector CLI
7Sageer Jul 20, 2026
921afea
docs: note the agent-file trust model and never-matching tool patterns
7Sageer Jul 20, 2026
a56d629
chore: slim changeset wording to user-facing language
7Sageer Jul 20, 2026
144c410
Merge remote-tracking branch 'upstream/main' into feat-custom-agent-file
7Sageer Jul 20, 2026
753fee4
chore: shorten changeset entries to one-line summaries
7Sageer Jul 20, 2026
5d4e059
Delete .changeset/v1-resume-v2-sessions.md
7Sageer Jul 20, 2026
b04e30c
test: drop class-instance spread in sessionLifecycle test stub
7Sageer Jul 20, 2026
599d9e2
chore: clear the comments
7Sageer Jul 20, 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
6 changes: 6 additions & 0 deletions .changeset/agent-file-subagents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core-v2": minor
"@moonshot-ai/kimi-code": minor
---

Let custom agent files restrict which sub-agent types they may delegate to (v2 engine only).
8 changes: 8 additions & 0 deletions .changeset/custom-agent-files.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@moonshot-ai/agent-core-v2": minor
"@moonshot-ai/kap-server": minor
"@moonshot-ai/protocol": minor
"@moonshot-ai/kimi-code": minor
---

Support custom agents defined as Markdown files with frontmatter, usable as the main agent or a sub-agent (v2 engine only).
11 changes: 11 additions & 0 deletions .changeset/global-tool-gating.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/agent-core-v2": minor
"@moonshot-ai/kap-server": minor
"@moonshot-ai/protocol": minor
"@moonshot-ai/klient": minor
"@moonshot-ai/kimi-code-sdk": minor
"@moonshot-ai/kimi-code": minor
---

Add global tool gating to constrain which tools agents may use, with a per-session override (v2 engine only).
6 changes: 6 additions & 0 deletions .changeset/system-md-override.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core-v2": minor
"@moonshot-ai/kimi-code": minor
---

Support overriding the default main-agent system prompt with a user-level file for every session (v2 engine only).
6 changes: 6 additions & 0 deletions .changeset/tool-pattern-warnings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core-v2": patch
"@moonshot-ai/kimi-code": patch
---

Warn when a tool allow/deny list entry can never match any tool, for example a misspelled name (v2 engine only).
31 changes: 30 additions & 1 deletion apps/kimi-code/src/cli/commands.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CLI_COMMAND_NAME } from '#/constant/app';
import { registerMigrateCommand } from '#/migration/index';
import { Command, Option } from 'commander';
import { Command, InvalidArgumentError, Option } from 'commander';

import type { CLIOptions } from './options';
import { registerAcpCommand } from './sub/acp';
Expand Down Expand Up @@ -74,6 +74,33 @@ export function createProgram(
.argParser((value: string, previous: string[] | undefined) => [...(previous ?? []), value])
.default([]),
)
.addOption(
new Option(
'--agent <name>',
'Agent profile to use for this invocation (v2 engine only). Custom profiles are discovered from agent directories or loaded via --agent-file.',
)
.argParser((value: string, previous: string | undefined) => {
if (previous !== undefined) {
throw new InvalidArgumentError('--agent may only be specified once.');
}
return value;
})
.conflicts('agentFile'),
)
.addOption(
new Option(
'--agent-file <path>',
'Load an agent definition from a Markdown file and select it (v2 engine only).',
)
.argParser((value: string, previous: string[] | undefined) => {
if ((previous?.length ?? 0) > 0) {
throw new InvalidArgumentError('--agent-file may only be specified once.');
}
return [value];
})
.conflicts('agent')
.default([]),
)
.addOption(
new Option(
'--add-dir <dir>',
Expand Down Expand Up @@ -133,6 +160,8 @@ export function createProgram(
outputFormat: raw['outputFormat'] as CLIOptions['outputFormat'],
prompt: raw['prompt'] as string | undefined,
skillsDirs: raw['skillsDir'] as string[],
agent: raw['agent'] as string | undefined,
agentFiles: raw['agentFile'] as string[],
addDirs: raw['addDir'] as string[],
};

Expand Down
24 changes: 24 additions & 0 deletions apps/kimi-code/src/cli/options.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isKimiV2Enabled } from './experimental-v2';

export type UIMode = 'shell' | 'print';
export type PromptOutputFormat = 'text' | 'stream-json';

Expand Down Expand Up @@ -44,6 +46,8 @@ export interface CLIOptions {
outputFormat: PromptOutputFormat | undefined;
prompt: string | undefined;
skillsDirs: string[];
agent: string | undefined;
agentFiles: string[];
addDirs?: string[];
}

Expand Down Expand Up @@ -83,6 +87,26 @@ export function validateOptions(
if (promptMode && opts.plan) {
throw new OptionConflictError('Cannot combine --prompt with --plan.');
}
if (opts.agent !== undefined && opts.agent.trim().length === 0) {
throw new OptionConflictError('Agent cannot be empty.');
}
if (opts.agentFiles.length > 1) {
throw new OptionConflictError('--agent-file may only be specified once.');
}
if (opts.agentFiles.some((file) => file.trim().length === 0)) {
throw new OptionConflictError('Agent file path cannot be empty.');
}
if (opts.agent !== undefined && opts.agentFiles.length > 0) {
throw new OptionConflictError('Cannot combine --agent with --agent-file.');
}
if (
(opts.agent !== undefined || opts.agentFiles.length > 0) &&
(!promptMode || !isKimiV2Enabled(env))
) {
throw new OptionConflictError(
'--agent/--agent-file are only available with the v2 engine (kimi -p with KIMI_CODE_EXPERIMENTAL_FLAG=1).',
);
}
if (promptMode && opts.session === '') {
throw new OptionConflictError('Cannot use --session without an id in prompt mode.');
}
Expand Down
81 changes: 74 additions & 7 deletions apps/kimi-code/src/cli/v2/run-v2-print.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
* Selected by `runPrompt` when `KIMI_CODE_EXPERIMENTAL_FLAG` is set.
*/

import { readFile } from 'node:fs/promises';

import {
IAgentGoalService,
IAgentLifecycleService,
Expand All @@ -24,17 +26,21 @@ import {
IAgentPromptService,
IAgentTaskService,
IAuthSummaryService,
IBootstrapService,
IConfigService,
IEventBus,
IOAuthToolkit,
ISessionIndex,
ISessionLifecycleService,
ITelemetryService,
agentCatalogRuntimeOptionsSeed,
bootstrap,
createCloudAppender,
ensureMainAgent,
hostRequestHeadersSeed,
logSeed,
parseAgentFileText,
resolveAgentPath,
resolveAgentTaskConfig,
resolveKimiHome,
resolveLoggingConfig,
Expand Down Expand Up @@ -120,6 +126,11 @@ export async function runV2Print(
// `--skillsDir` (v1 print parity): explicit skill dirs replace default
// user / project discovery for this process.
...skillCatalogRuntimeOptionsSeed(opts.skillsDirs),
// `--agent-file`: explicit agent definition files, registered with the
// highest-precedence source for this process. Passed through unresolved —
// the engine expands `~` and resolves relative paths against the session
// workDir (mirroring `--skills-dir`).
...agentCatalogRuntimeOptionsSeed(opts.agentFiles),
]);
const auth = app.accessor.get(IOAuthToolkit);

Expand Down Expand Up @@ -236,6 +247,63 @@ async function resolveNativeSession(
const lifecycle = app.accessor.get(ISessionLifecycleService);
const index = app.accessor.get(ISessionIndex);

// `--agent` selects a catalog profile by name; otherwise `--agent-file`
// implicitly selects the profile that file defines. The file
// is parsed here (fatal on error) so a bad file fails before any turn.
let agentProfileName = opts.agent;
const agentFile = opts.agentFiles[0];
if (agentProfileName === undefined && agentFile !== undefined) {
const agentFilePath = resolveAgentPath(
agentFile,
workDir,
app.accessor.get(IBootstrapService).osHomeDir,
);
let agentFileText: string;
try {
agentFileText = await readFile(agentFilePath, 'utf8');
} catch (error) {
throw new Error(
`Failed to read agent file "${agentFilePath}": ${error instanceof Error ? error.message : String(error)}`,
{ cause: error },
);
}
try {
agentProfileName = parseAgentFileText({
path: agentFilePath,
source: 'explicit',
text: agentFileText,
}).name;
} catch (error) {
throw new Error(
`Invalid agent file "${agentFilePath}": ${error instanceof Error ? error.message : String(error)}`,
{ cause: error },
);
}
}

// `--agent` / `--agent-file` bind an explicit profile; without them the
// historical setModel path (default profile on first bind) is kept. A
// same-name re-select on a resumed session keeps the profile and only applies
// an explicitly requested model; a different name is rejected by the
// engine's first-bind guard inside `bind`.
const applyProfileSelection = async (
profile: IAgentProfileService,
model: string | undefined,
): Promise<void> => {
if (agentProfileName !== undefined) {
if (profile.data().profileName === agentProfileName) {
if (model !== undefined) await profile.setModel(model);
return;
}
await profile.bind({
profile: agentProfileName,
model: requireConfiguredModel(model ?? profile.getModel(), defaultModel),
});
} else if (model !== undefined) {
await profile.setModel(model);
}
};

const resumeById = async (id: string): Promise<ISessionScopeHandle> => {
const session = await lifecycle.resume(id);
if (session === undefined) {
Expand Down Expand Up @@ -273,9 +341,7 @@ async function resolveNativeSession(
const session = await resumeById(opts.session);
const agent = await ensureMainAgent(session);
const profile = agent.accessor.get(IAgentProfileService);
if (opts.model !== undefined) {
await profile.setModel(opts.model);
}
await applyProfileSelection(profile, opts.model);
const currentModel = profile.getModel();
const { restorePermission } = forceAuto(agent);
return {
Expand All @@ -294,9 +360,7 @@ async function resolveNativeSession(
const session = await resumeById(previous.id);
const agent = await ensureMainAgent(session);
const profile = agent.accessor.get(IAgentProfileService);
if (opts.model !== undefined) {
await profile.setModel(opts.model);
}
await applyProfileSelection(profile, opts.model);
const currentModel = profile.getModel();
const { restorePermission } = forceAuto(agent);
return {
Expand All @@ -314,9 +378,12 @@ async function resolveNativeSession(
const session = await lifecycle.create({
workDir,
additionalDirs: opts.addDirs?.length ? opts.addDirs : undefined,
mainAgentBinding: {
profile: agentProfileName ?? 'agent',
model,
},
});
const agent = await ensureMainAgent(session);
await agent.accessor.get(IAgentProfileService).setModel(model);
agent.accessor.get(IAgentPermissionModeService).setMode('auto');
return {
session,
Expand Down
2 changes: 2 additions & 0 deletions apps/kimi-code/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ const MIGRATE_CLI_OPTIONS: CLIOptions = {
outputFormat: undefined,
prompt: undefined,
skillsDirs: [],
agent: undefined,
agentFiles: [],
};

export function main(): void {
Expand Down
2 changes: 2 additions & 0 deletions apps/kimi-code/test/cli/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ function defaultOpts(): CLIOptions {
outputFormat: undefined,
prompt: undefined,
skillsDirs: [],
agent: undefined,
agentFiles: [],
};
}

Expand Down
Loading
Loading