feat: support custom agent files#1735
Open
7Sageer wants to merge 26 commits into
Open
Conversation
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 detectedLatest commit: 2c10985 The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
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 |
commit: |
- 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-fileflags, 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):
name,description,whenToUse,mode: replace|append,tools,disallowedTools) + prompt body, aligned with the claude-code convention.mode: appendinjects the body into the builtin prompt'sROLE_ADDITIONALslot instead of replacing the whole prompt.$KIMI_CODE_HOME/agents/,~/.agents/agents/,.kimi-code/agents/,.agents/agents/(walking up to.git), plus anextra_agent_dirsconfig section. Same-name override priority: builtin < user < extra < project < explicit (--agent-filewins by design).agentFileCatalogapp domain (discovery / parsing / profile factory) and a session-scoped mergedSessionAgentProfileCatalogconsulted by profile binding, the Agent (subagent) tool, and swarm spawning.AgentProfile.toolsbecomes optional (undefined= all tools) and gainsdisallowedTools; the deny gate is evaluated inisToolActiveand 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.--agent <name>/--agent-file <path>for the v2 print runner (kimi -pwithKIMI_CODE_EXPERIMENTAL_FLAG). The v1 TUI rejects them with a clear v2-only error.profilefield 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-fileis local-machine only; remote upload is future work.$KIMI_CODE_HOME/SYSTEM.mdpermanently 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: builtintoolsanddescriptionare 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.[tools]config section —enabledis a global allowlist (empty/absent = unconstrained, so an empty list never disables everything),disableda denylist applied on top. Both are intersected with the active profile's policy inisToolActive, withmcp__glob support. All existing consumers (RPC tool listing, subagent tools, Task checks, todo, tool-select) pick it up without changes.disabled_tools(REST) /disabledTools(RPC), applied through the newprofileService.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 withprofile.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
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.