Skip to content

feat: support custom agent files#1735

Open
7Sageer wants to merge 26 commits into
MoonshotAI:mainfrom
7Sageer:feat-custom-agent-file
Open

feat: support custom agent files#1735
7Sageer wants to merge 26 commits into
MoonshotAI:mainfrom
7Sageer:feat-custom-agent-file

Conversation

@7Sageer

@7Sageer 7Sageer commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

No tracking issue — the problem is explained below.

Problem

There is no way to customize agents: users cannot define a custom system prompt, append to the builtin one, or restrict an agent's tool set. There is also no permanent system prompt override (the CLI flags are per-launch), no global way to gate tools, and no protocol surface for clients to restrict tools per session. The CLI once had --agent / --agent-file flags, but they were removed and currently sit on the rejected-options test list (apps/kimi-code/test/cli/options.test.ts). This PR intentionally brings them back in a more complete form.

What changed

Implements file-defined custom agents on the agent-core-v2 engine (v1 intentionally out of scope):

  • Format: one Markdown file per agent — frontmatter (name, description, whenToUse, mode: replace|append, tools, disallowedTools) + prompt body, aligned with the claude-code convention. mode: append injects the body into the builtin prompt's ROLE_ADDITIONAL slot instead of replacing the whole prompt.
  • Discovery: mirrors the skill system's roots — $KIMI_CODE_HOME/agents/, ~/.agents/agents/, .kimi-code/agents/, .agents/agents/ (walking up to .git), plus an extra_agent_dirs config section. Same-name override priority: builtin < user < extra < project < explicit (--agent-file wins by design).
  • Engine (agent-core-v2): new agentFileCatalog app domain (discovery / parsing / profile factory) and a session-scoped merged SessionAgentProfileCatalog consulted by profile binding, the Agent (subagent) tool, and swarm spawning. AgentProfile.tools becomes optional (undefined = all tools) and gains disallowedTools; the deny gate is evaluated in isToolActive and persisted in session wire records, so a resumed session keeps the gate even if the agent file was deleted. Non-fatal source load failures degrade to warn + keep previous contributions instead of poisoning the session.
  • CLI: restores --agent <name> / --agent-file <path> for the v2 print runner (kimi -p with KIMI_CODE_EXPERIMENTAL_FLAG). The v1 TUI rejects them with a clear v2-only error.
  • Server (kap-server / protocol): optional profile field on prompt submission with first-bind semantics (re-sending the same name is a no-op; switching names is rejected with the available profiles listed). --agent-file is local-machine only; remote upload is future work.
  • Permanent system prompt override: $KIMI_CODE_HOME/SYSTEM.md permanently replaces the builtin default system prompt — no per-launch flag needed. It is injected as the default main-agent profile (name: agent, override: true) and only replaces the prompt: builtin tools and description are inherited. The body supports variable substitution (${skills}, ${agents_md}, ${cwd}, ${cwd_listing}, ${os}, ${shell}, ${now}); unknown ${...} variables pass through verbatim. Missing or empty files are ignored; unreadable files warn and fall back to the builtin profile. Priority: --agent-file / --agent / project override > SYSTEM.md > same-name user-scope scan files.
  • Global tool gating: new [tools] config section — enabled is a global allowlist (empty/absent = unconstrained, so an empty list never disables everything), disabled a denylist applied on top. Both are intersected with the active profile's policy in isToolActive, with mcp__ glob support. All existing consumers (RPC tool listing, subagent tools, Task checks, todo, tool-select) pick it up without changes.
  • Session tool gating (protocol / SDK): prompt submission accepts disabled_tools (REST) / disabledTools (RPC), applied through the new profileService.setSessionDisabledTools — it replaces the client-owned denylist, keeps the profile's own deny, persists across session resume, and rejects calls before a profile is bound with profile.not_bound (mapped to 40001). Plumbed through the v2 RPC, protocol and kap-server schemas, klient contract/facade, and node-sdk; v1 core-api gains the field type-only and ignores it.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

Discover Markdown+frontmatter agent definitions from user, project, and configured directories; merge them into a session-level profile catalog (priority: builtin < user < extra < project < explicit). Custom agents work as subagents via the Agent tool and as the main agent.

- agent-core-v2: new agentFileCatalog domain (discovery/parsing/profile factory, mirroring skillRoots/parseFrontmatter) and sessionAgentProfileCatalog merged view; AgentProfile.tools becomes optional (undefined = all tools) and gains disallowedTools deny list, evaluated in profileService.isToolActive and persisted in session wire records so resume keeps the gate even if the file is gone
- CLI: restore --agent/--agent-file for the v2 print runner (KIMI_CODE_EXPERIMENTAL_FLAG); the v1 TUI rejects them with a clear v2-only error
- kap-server/protocol: optional profile field on prompt submission with first-bind semantics (same-name no-op, different name rejected)
- docs: custom agents section (en/zh) + config/CLI references + changeset
@changeset-bot

changeset-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2c10985

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@moonshot-ai/agent-core Patch
@moonshot-ai/agent-core-v2 Minor
@moonshot-ai/kap-server Minor
@moonshot-ai/protocol Minor
@moonshot-ai/kimi-code Minor
@moonshot-ai/klient Minor
@moonshot-ai/kimi-code-sdk Minor
@moonshot-ai/acp-adapter Patch
kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jul 15, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@ce5d2e3
npx https://pkg.pr.new/@moonshot-ai/kimi-code@ce5d2e3

commit: ce5d2e3

7Sageer added 24 commits July 15, 2026 13:51
- agent-core-v2: bind now owns the first-bind invariant — switching
  profiles after bind throws profile.already_bound (checked again in the
  synchronous segment before the first wire dispatch, so concurrent binds
  cannot both pass); unknown names throw profile.unknown; same-name
  rebinds keep the persisted thinking effort.
- kap-server / CLI: edges degrade to error mapping / same-name no-op
  instead of their own divergent guards.
- agent files: reject non-string mode values, honor disallowedTools in
  the append-mode Skill probe, pass --agent-file through unresolved so
  the engine can expand ~, reject empty --agent-file values.
- session catalog: ready is recoverable via reload() after a fatal
  source failure, and agent-file discovery is kicked at session
  materialize so resumed sessions see file agents from the first turn.
- docs: first-bind semantics, name: agent override, tools: [] meaning,
  --agent-file last-wins.
…le by v1

Binding a profile without a tool allowlist (the default for file-defined
custom agents) persisted a tools.set_active_tools record with no `names`
key. v1 clients discover v2 sessions through the shared session index and
replay newer wire versions without migration, so the record crashed v1
resume with a TypeError and wedged the session permanently.

The v2 engine no longer writes the record when the base set is already
"every tool active" (its absence encodes the same state), and v1 replay
now skips records that lack `names` as defense in depth for wires already
written by preview builds. A same-name rebind that resets an allowlist to
"all tools" has no v1-safe encoding and is left as a documented gap (no
production caller today); a future tools.reset_active_tools Op is safe
because v1 silently no-ops unknown record types.
…covery

A single unreadable subdirectory (EACCES anywhere in the scanned tree)
previously aborted the whole discovery pass, zeroing every agent of that
source on every session start with one path-less warning. The walker now
skips-and-warns per directory below the root (mirroring the skill
discovery it parallels), root-level failures are isolated per root so one
bad root no longer takes its siblings down, and only a genuinely transient
whole-fs outage (os.fs.unavailable) still propagates so the session
catalog keeps its previous contribution. Source-level warnings now name
the offending path, and repeated skip warnings are capped with a summary
that samples the suppressed paths.

Also consolidates the path primitives (~ expansion, base-relative
resolution, realpath type probes) shared by the root resolvers, the
walker, and the explicit-file source into agentFileCatalog/paths.ts, and
tightens parser diagnostics: frontmatter null is treated as absent, and a
present-but-wrong-typed name/description reports a type error instead of
"missing".
…rofile

A directory-discovered agent file colliding with a builtin profile
without override: true was silently dropped at merge time. The suppression
now logs a warning naming the profile and the opt-in.
…rompt

The third parameter was a full tool list used only for includes('Skill'),
which forced the agent-file profile factory to answer a boolean question
with sentinel lists. The template now takes an explicit skillActive flag;
a skillActiveFor helper keeps builtin call sites derived from their tool
arrays.
… bind

BindAgentInput.model is now optional: the engine resolves a missing model
against the configured defaultModel and throws model.not_configured when
neither is set, so edges no longer each re-implement the fallback.
… at first bind

A REST prompt carrying profile + an unsupported thinking effort bound the
session first and failed setThinking after, wedging the session on an
identity the user never successfully used. The effort is now validated up
front when the caller marks it as an explicit request (strictThinking):
the bind rejects before any await or state mutation, and the requested
effort rides along in the bind instead of a separate setThinking. Internal
spawn/fork paths pass inherited thinking without the flag and keep the
previous clamp behavior — a persisted effort that drifted out of the
model's support list must not break subagent spawning. The route's
now-redundant model fallback is dropped in favor of the engine-side
default.
…alize

The catalog's ready promise was only kicked, so a resumed session's first
turn could render the Agent tool description without the file-defined
agent types. Discovery is local-fs and cheap, so materialize now awaits
it; ready only rejects for a fatal explicit-source error, which is exactly
the case that should fail fast. A failure there now also removes and
disposes the half-materialized handle instead of leaving it registered in
the session cache.
… hygiene

The v2 print CLI now has a test asserting an invalid --agent-file fails
before any turn. The denylist profiles in binding.test.ts register in a
beforeAll (idempotent, scoped to the describe's run window) instead of at
module scope during collection.
--agent/--agent-file are rejected without the v2 engine, so restore the
requirement in the Agents and Command Reference pages (and use
KIMI_CODE_EXPERIMENTAL_FLAG=1 in the examples), note that tool lists only
shape model-visible disclosure (permission rules are the enforcement
layer), and remind authors of delegation-bound agents to state the
handoff contract in the prompt body.
Keep these files' diff limited to the realpath fakes the feature needs;
the lint-preference rewrites belong to a separate cleanup.
# Conflicts:
#	packages/agent-core-v2/src/agent/profile/profileService.ts
#	packages/agent-core-v2/src/os/backends/node-local/hostFsService.ts
#	packages/agent-core-v2/src/os/interface/hostFileSystem.ts
# Conflicts:
#	packages/agent-core-v2/src/session/subagent/tools/agent.ts
Read $KIMI_CODE_HOME/SYSTEM.md on every startup and inject it as the default main-agent profile (name "agent", override: true), replacing the builtin default system prompt while inheriting builtin tools and description. Missing or empty files are ignored; unreadable files warn and fall back to the builtin profile.

The body supports variable substitution (${skills}, ${agents_md}, ${cwd}, ${cwd_listing}, ${os}, ${shell}, ${now}); unknown variables pass through verbatim.

Priority: --agent-file / --agent / project override > SYSTEM.md > same-name user-scope scan files.
…ools

Add a [tools] config section: "enabled" acts as a global allowlist (empty = unconstrained), "disabled" as a denylist applied on top, both intersected with the active profile's policy in isToolActive (mcp glob supported).

Plumb a session-persistent disabledTools parameter through the stack: v2 RPC PromptPayload, REST "disabled_tools" (protocol and kap-server parallel schemas), klient contract/facade, and node-sdk. The server applies it via profileService.setSessionDisabledTools, which replaces the client-owned denylist, keeps the profile's own deny, persists across resume, and rejects calls before a profile is bound with profile.not_bound (mapped to 40001). v1 core-api gains a type-only field and ignores it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant