mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-04 22:11:27 +00:00
Merge remote-tracking branch 'origin/main' into feat-skill-reminder
# Conflicts: # packages/agent-core-v2/docs/state-manifest.d.ts # packages/agent-core-v2/src/agent/profile/profileService.ts # packages/agent-core-v2/src/session/sessionSkillCatalog/pluginSkillSource.ts # packages/agent-core-v2/test/agent/plugin/agentPlugin.test.ts # packages/agent-core-v2/test/agent/profile/profileOps.test.ts
This commit is contained in:
commit
30ffe3023c
343 changed files with 9768 additions and 1037 deletions
|
|
@ -11,7 +11,7 @@ Gate not-yet-public features behind `IFlagService.enabled(id)`, per the reposito
|
|||
- `src/flag/flag.ts` — `IFlagService` token + resolver types (`ExperimentalFlagMap`, `ExperimentalFlagConfig`, `ExperimentalFlagSource`, `ExperimentalFeatureState`) + `ExperimentalConfigSchema` / `ExperimentalConfig` (zod).
|
||||
- `src/flag/flagService.ts` — `FlagService` impl + `MASTER_ENV` (`KIMI_CODE_EXPERIMENTAL_FLAG`) + `EXPERIMENTAL_SECTION` (`experimental`); reads definitions from `IFlagRegistry`; self-registers at App scope.
|
||||
- `src/flag/index.ts` — **removed (no barrel)**; `src/index.ts` imports the `flag` leafs precisely instead (e.g. `import './flag/flagService'`).
|
||||
- `src/<domain>/flag.ts` — each domain that owns a flag declares it here and calls `registerFlagDefinition` at the module top level (e.g. `src/agent/toolSelect/flag.ts` or `src/agent/faultInjection/flag.ts`). The directory already names the domain, so the file is just `flag.ts`.
|
||||
- `src/<domain>/flag.ts` — each domain that owns a flag declares it here and calls `registerFlagDefinition` at the module top level (e.g. `src/agent/toolSelect/flag.ts`). The directory already names the domain, so the file is just `flag.ts`.
|
||||
|
||||
## Public surface
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Add an internal bash parsing capability that turns shell command strings into syntax trees, in preparation for per-command permission analysis. No user-facing behavior change yet.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"@moonshot-ai/agent-core": patch
|
||||
---
|
||||
|
||||
Count validation-rejected tool calls toward the repeat breaker so reminders fire at 3/5/8 and the turn force-stops at 12.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"@moonshot-ai/agent-core-v2": patch
|
||||
---
|
||||
|
||||
Let embedding hosts customize the agent's product name and reply-style guidance in the system prompt when starting the server.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Show a quota consumption note after installing official plugins that bill against plan quota (such as Kimi Datasource).
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Show an update notice when a turn that used an outdated plugin ends and the Official Marketplace has a newer version; each new version is announced once. Run /plugins to install the latest version.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
"@moonshot-ai/kimi-code-sdk": patch
|
||||
---
|
||||
|
||||
Fail fast on quota/balance-exhausted HTTP 429 errors (e.g. Moonshot `exceeded_current_quota_error`, OpenAI `insufficient_quota`) instead of silently retrying for ~3 minutes. Transient rate-limit 429s keep the existing retry, backoff, and Retry-After behavior.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
'@moonshot-ai/kimi-code': minor
|
||||
---
|
||||
|
||||
Customizable footer status line: compose built-in slots via `[status_line] items` in `tui.toml`, or render the first stdout line of a user script via `[status_line] command`.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code-oauth": patch
|
||||
"@moonshot-ai/agent-core-v2": patch
|
||||
"@moonshot-ai/kap-server": patch
|
||||
---
|
||||
|
||||
Derive the /usage plan usage window labels and reset hints from structured usage data instead of preformatted text.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
web: Fix garbled line numbers in code blocks.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": minor
|
||||
---
|
||||
|
||||
Remove the 50 MB size limit on file uploads to the built-in server, so large attachments (for example in the web UI) no longer fail with an upload-too-large error. Uploads now stream to disk instead of being buffered in memory.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"@moonshot-ai/agent-core-v2": patch
|
||||
---
|
||||
|
||||
Count validation-rejected tool calls toward the repeat breaker so reminders fire at 3/5/8 and the turn force-stops at 12.
|
||||
|
|
@ -1,5 +1,45 @@
|
|||
# @moonshot-ai/kimi-code
|
||||
|
||||
## 0.31.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#2365](https://github.com/MoonshotAI/kimi-code/pull/2365) [`fa2c5ce`](https://github.com/MoonshotAI/kimi-code/commit/fa2c5ce18b70577fa3ada4eb8bdd4993891994ce) Thanks [@7Sageer](https://github.com/7Sageer)! - Add support for plugin-contributed custom agents, discovered automatically and available for sub-agent delegation. Ship an `agents/` directory in the plugin (or declare `agents` paths in the plugin manifest) to provide them.
|
||||
|
||||
- [#2314](https://github.com/MoonshotAI/kimi-code/pull/2314) [`02d77b2`](https://github.com/MoonshotAI/kimi-code/commit/02d77b20d941873563f14890e049ffe40cec76e4) Thanks [@7Sageer](https://github.com/7Sageer)! - Allow enabled plugins to contribute agent system-prompt instructions through `systemPrompt` or `systemPromptPath` in `kimi.plugin.json`, effective on both agent engines (the TUI, `kimi -p`, and `kimi web`).
|
||||
|
||||
- [#2232](https://github.com/MoonshotAI/kimi-code/pull/2232) [`efac96c`](https://github.com/MoonshotAI/kimi-code/commit/efac96c8a95a3c3ca4e1ae9bce38082498a02b2e) Thanks [@7Sageer](https://github.com/7Sageer)! - Support Markdown-defined custom agents on agent-core.
|
||||
|
||||
- [#2232](https://github.com/MoonshotAI/kimi-code/pull/2232) [`efac96c`](https://github.com/MoonshotAI/kimi-code/commit/efac96c8a95a3c3ca4e1ae9bce38082498a02b2e) Thanks [@7Sageer](https://github.com/7Sageer)! - Add the /secondary_model slash command to configure the secondary model used by subagents.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#2382](https://github.com/MoonshotAI/kimi-code/pull/2382) [`40172c7`](https://github.com/MoonshotAI/kimi-code/commit/40172c7ca96ca981b043b793588dd32e898979fa) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix request headers not being passed correctly on some requests.
|
||||
|
||||
- [#2379](https://github.com/MoonshotAI/kimi-code/pull/2379) [`691ec46`](https://github.com/MoonshotAI/kimi-code/commit/691ec4679ea19d6be8ac18f359088384ed3e446d) Thanks [@RealKai42](https://github.com/RealKai42)! - Remove the blocking `block`/`timeout` wait from the TaskOutput tool so checking a background task can no longer stall the conversation; it now always returns an immediate snapshot, and completion still arrives via automatic notification.
|
||||
|
||||
- [#2395](https://github.com/MoonshotAI/kimi-code/pull/2395) [`d10b1c1`](https://github.com/MoonshotAI/kimi-code/commit/d10b1c130813dbd6ee8c8599a6a98feb36aea67f) Thanks [@sailist](https://github.com/sailist)! - Fix sessions missing from the session picker when their cached metadata predates the archived flag.
|
||||
|
||||
## 0.30.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#2255](https://github.com/MoonshotAI/kimi-code/pull/2255) [`67dd031`](https://github.com/MoonshotAI/kimi-code/commit/67dd03149f36be91a0c081e70d8a2d721b0f1c64) Thanks [@he-yufeng](https://github.com/he-yufeng)! - Add a customizable footer status line, configured via `[status_line]` in `tui.toml`.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#2313](https://github.com/MoonshotAI/kimi-code/pull/2313) [`de0ba9d`](https://github.com/MoonshotAI/kimi-code/commit/de0ba9d0654273ff6b028a7a561983ebee4e723e) Thanks [@starquakee](https://github.com/starquakee)! - Stop the turn after repeated invalid tool calls instead of retrying indefinitely.
|
||||
|
||||
- [#2147](https://github.com/MoonshotAI/kimi-code/pull/2147) [`29783e4`](https://github.com/MoonshotAI/kimi-code/commit/29783e471afcf7975852e496907646458264d2e6) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Show a quota note after installing official plugins that bill against plan quota (such as Kimi Datasource).
|
||||
|
||||
- [#2147](https://github.com/MoonshotAI/kimi-code/pull/2147) [`29783e4`](https://github.com/MoonshotAI/kimi-code/commit/29783e471afcf7975852e496907646458264d2e6) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Show a notice when an official plugin used in the session has an update available. Run /plugins to install it.
|
||||
|
||||
- [#1857](https://github.com/MoonshotAI/kimi-code/pull/1857) [`cdbd33c`](https://github.com/MoonshotAI/kimi-code/commit/cdbd33c13c7f5cd4c49ec112ee4313b3938a7752) Thanks [@vinlee19](https://github.com/vinlee19)! - Fail fast when account quota or balance is exhausted instead of silently retrying for ~3 minutes.
|
||||
|
||||
- [#2294](https://github.com/MoonshotAI/kimi-code/pull/2294) [`425cfdf`](https://github.com/MoonshotAI/kimi-code/commit/425cfdf53f0fd3b01527f5fba87acff68f49f368) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix garbled line numbers in code blocks.
|
||||
|
||||
- [#2312](https://github.com/MoonshotAI/kimi-code/pull/2312) [`d03a488`](https://github.com/MoonshotAI/kimi-code/commit/d03a4886fdf7c35014c10079a3d417aeb0447d9a) Thanks [@sailist](https://github.com/sailist)! - Remove the 50 MB size limit on file uploads to the built-in server.
|
||||
|
||||
## 0.29.2
|
||||
|
||||
### Patch Changes
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@moonshot-ai/kimi-code",
|
||||
"version": "0.29.2",
|
||||
"version": "0.31.0",
|
||||
"description": "The Starting Point for Next-Gen Agents",
|
||||
"license": "MIT",
|
||||
"author": "Moonshot AI",
|
||||
|
|
|
|||
42
apps/kimi-code/src/cli/agent-selection.ts
Normal file
42
apps/kimi-code/src/cli/agent-selection.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { readFile } from 'node:fs/promises';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
import { parseAgentFileText, resolveAgentPath } from '@moonshot-ai/kimi-code-sdk';
|
||||
|
||||
import type { CLIOptions } from './options';
|
||||
|
||||
/**
|
||||
* Resolve which agent profile the launch flags select.
|
||||
*
|
||||
* `--agent` carries the profile name directly; `--agent-file` implicitly
|
||||
* selects the profile the file defines, so the file is parsed here (fatal on
|
||||
* error) so a bad file fails before any session work. Returns undefined when
|
||||
* neither flag is present.
|
||||
*/
|
||||
export async function resolveAgentProfileSelection(
|
||||
opts: Pick<CLIOptions, 'agent' | 'agentFiles'>,
|
||||
workDir: string,
|
||||
): Promise<string | undefined> {
|
||||
if (opts.agent !== undefined) return opts.agent;
|
||||
const agentFile = opts.agentFiles?.[0];
|
||||
if (agentFile === undefined) return undefined;
|
||||
|
||||
const path = resolveAgentPath(agentFile, workDir, homedir());
|
||||
let text: string;
|
||||
try {
|
||||
text = await readFile(path, 'utf8');
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to read agent file "${path}": ${error instanceof Error ? error.message : String(error)}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
try {
|
||||
return parseAgentFileText({ path, source: 'explicit', text }).name;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Invalid agent file "${path}": ${error instanceof Error ? error.message : String(error)}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@ export function createProgram(
|
|||
.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.',
|
||||
'Agent profile to start the new session with. Custom profiles are discovered from agent directories or loaded via --agent-file. Cannot be combined with --session/--continue.',
|
||||
)
|
||||
.argParser((value: string, previous: string | undefined) => {
|
||||
if (previous !== undefined) {
|
||||
|
|
@ -90,7 +90,7 @@ export function createProgram(
|
|||
.addOption(
|
||||
new Option(
|
||||
'--agent-file <path>',
|
||||
'Load an agent definition from a Markdown file and select it (v2 engine only).',
|
||||
'Load an agent definition from a Markdown file and select it for the new session. Cannot be combined with --session/--continue.',
|
||||
)
|
||||
.argParser((value: string, previous: string[] | undefined) => {
|
||||
if ((previous?.length ?? 0) > 0) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { isKimiV2Enabled } from './experimental-v2';
|
||||
|
||||
export type UIMode = 'shell' | 'print';
|
||||
export type PromptOutputFormat = 'text' | 'stream-json';
|
||||
|
||||
|
|
@ -101,10 +99,10 @@ export function validateOptions(
|
|||
}
|
||||
if (
|
||||
(opts.agent !== undefined || opts.agentFiles.length > 0) &&
|
||||
(!promptMode || !isKimiV2Enabled(env))
|
||||
(opts.session !== undefined || opts.continue)
|
||||
) {
|
||||
throw new OptionConflictError(
|
||||
'--agent/--agent-file are only available with the v2 engine (kimi -p with KIMI_CODE_EXPERIMENTAL_FLAG=1).',
|
||||
'Cannot combine --agent/--agent-file with --session/--continue: the agent is bound at session creation and the bound agent is restored automatically on resume.',
|
||||
);
|
||||
}
|
||||
if (promptMode && opts.session === '') {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { resolve } from 'pathe';
|
|||
|
||||
import { CLI_SHUTDOWN_TIMEOUT_MS, PROMPT_CLEANUP_TIMEOUT_MS } from '#/constant/app';
|
||||
|
||||
import { resolveAgentProfileSelection } from './agent-selection';
|
||||
import { isKimiV2Enabled } from './experimental-v2';
|
||||
import { resolveOutputFormat } from './options';
|
||||
import type { CLIOptions, PromptOutputFormat } from './options';
|
||||
|
|
@ -296,6 +297,9 @@ async function resolvePromptSession(
|
|||
stderr: PromptOutput,
|
||||
setRestorePermission: (restorePermission: () => Promise<void>) => void,
|
||||
): Promise<ResolvedPromptSession> {
|
||||
// `--agent`/`--agent-file` are creation-only: validateOptions rejects them
|
||||
// together with --session/--continue, so resume paths never forward a
|
||||
// profile — the bound agent is restored from the session itself.
|
||||
if (opts.session !== undefined) {
|
||||
const sessions = await harness.listSessions({ sessionId: opts.session, workDir });
|
||||
const target = sessions[0];
|
||||
|
|
@ -365,12 +369,15 @@ async function resolvePromptSession(
|
|||
stderr.write(`No sessions to continue under "${workDir}"; starting a fresh session.\n`);
|
||||
}
|
||||
|
||||
const agentProfile = await resolveAgentProfileSelection(opts, workDir);
|
||||
const model = requireConfiguredModel(opts.model, defaultModel);
|
||||
const session = await harness.createSession({
|
||||
workDir,
|
||||
model,
|
||||
permission: 'auto',
|
||||
additionalDirs: opts.addDirs?.length ? opts.addDirs : undefined,
|
||||
agentProfile,
|
||||
agentFiles: opts.agentFiles?.length ? opts.agentFiles : undefined,
|
||||
drainAgentTasksOnStop: true,
|
||||
});
|
||||
installHeadlessHandlers(session);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { toTerminalHyperlink } from '#/utils/terminal-hyperlink';
|
|||
import { restoreTerminalModes } from '#/utils/terminal-restore';
|
||||
|
||||
import type { CLIOptions } from './options';
|
||||
import { resolveAgentProfileSelection } from './agent-selection';
|
||||
import { isKimiV2Enabled } from './experimental-v2';
|
||||
import { createCliTelemetryBootstrap, initializeCliTelemetry } from './telemetry';
|
||||
import { createKimiCodeHostIdentity } from './version';
|
||||
|
|
@ -110,8 +111,12 @@ export async function runShell(
|
|||
configWarning = combineStartupNotice(configWarning, warning);
|
||||
}
|
||||
const configMs = Date.now() - configStartedAt;
|
||||
// Resolve --agent/--agent-file once for the startup session; validateOptions
|
||||
// has already rejected them alongside --session/--continue.
|
||||
const agentProfile = await resolveAgentProfileSelection(opts, workDir);
|
||||
const tui = new KimiTUI(harness, {
|
||||
cliOptions: opts,
|
||||
agentProfile,
|
||||
additionalDirs: opts.addDirs?.length ? opts.addDirs : undefined,
|
||||
tuiConfig,
|
||||
version,
|
||||
|
|
|
|||
|
|
@ -11,13 +11,12 @@
|
|||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { hostRequestHeadersSeed } from '@moonshot-ai/agent-core-v2';
|
||||
import { createServerLogger, startServer, type ServerLogger } from '@moonshot-ai/kap-server';
|
||||
import { shutdownTelemetry, track } from '@moonshot-ai/kimi-telemetry';
|
||||
import chalk from 'chalk';
|
||||
import { type Command } from 'commander';
|
||||
|
||||
import { CLI_SHUTDOWN_TIMEOUT_MS } from '#/constant/app';
|
||||
import { CLI_SHUTDOWN_TIMEOUT_MS, WEB_USER_AGENT_SUFFIX } from '#/constant/app';
|
||||
import { getNativeWebAssetsDir } from '#/native/web-assets';
|
||||
import { darkColors } from '#/tui/theme/colors';
|
||||
import { openUrl as defaultOpenUrl } from '#/utils/open-url';
|
||||
|
|
@ -25,7 +24,7 @@ import { getDataDir } from '#/utils/paths';
|
|||
|
||||
import { initializeServerTelemetry } from '../../telemetry';
|
||||
import {
|
||||
buildKimiDefaultHeaders,
|
||||
createKimiCodeHostIdentity,
|
||||
getHostPackageRoot,
|
||||
getVersion,
|
||||
} from '../../version';
|
||||
|
|
@ -278,7 +277,16 @@ async function runServerInProcess(
|
|||
port: options.port,
|
||||
// Report the CLI's product version as `server_version` (/meta, web UI)
|
||||
// rather than kap-server's private package version.
|
||||
version,
|
||||
serverVersion: version,
|
||||
// The CLI's host identity: feeds the engine's bootstrap client identity
|
||||
// and the derived outbound headers (User-Agent + X-Msh-*), so web-UI
|
||||
// OAuth flows and model / WebSearch requests carry the CLI identity. The
|
||||
// `web` User-Agent suffix distinguishes web-UI traffic from direct CLI
|
||||
// runs upstream (same product token, same platform).
|
||||
hostIdentity: {
|
||||
...createKimiCodeHostIdentity(version),
|
||||
userAgentSuffix: WEB_USER_AGENT_SUFFIX,
|
||||
},
|
||||
logLevel: options.logLevel,
|
||||
logger,
|
||||
debugEndpoints: options.debugEndpoints,
|
||||
|
|
@ -291,10 +299,6 @@ async function runServerInProcess(
|
|||
// `telemetry` toggle). Complements the v1 client registered above, which
|
||||
// only covers host-level events.
|
||||
telemetry: true,
|
||||
// Seed the CLI's Kimi identity headers so the engine's outbound
|
||||
// requests (model, WebSearch, FetchURL) carry the same User-Agent +
|
||||
// X-Msh-* identity as direct CLI runs.
|
||||
seeds: hostRequestHeadersSeed(buildKimiDefaultHeaders(version)),
|
||||
webAssetsDir,
|
||||
});
|
||||
logger.info('serving the REST/WS API and the bundled web UI');
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ export async function runV2Print(
|
|||
const identity = createKimiCodeHostIdentity(version);
|
||||
const hostHeaders = createKimiDefaultHeaders({ homeDir, ...identity });
|
||||
|
||||
const { app } = bootstrap({ homeDir, clientVersion: version }, [
|
||||
const { app } = bootstrap({ homeDir, clientIdentity: identity }, [
|
||||
...logSeed(logging),
|
||||
...hostRequestHeadersSeed(hostHeaders),
|
||||
// `--skillsDir` (v1 print parity): explicit skill dirs replace default
|
||||
|
|
@ -293,27 +293,14 @@ async function resolveNativeSession(
|
|||
}
|
||||
}
|
||||
|
||||
// `--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 (
|
||||
// `--agent` / `--agent-file` are creation-only: validateOptions rejects them
|
||||
// together with --session/--continue, so resume paths only apply an
|
||||
// explicitly requested model — the bound profile is restored by the engine.
|
||||
const applyModelOverride = 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);
|
||||
}
|
||||
if (model !== undefined) await profile.setModel(model);
|
||||
};
|
||||
|
||||
const resumeById = async (id: string): Promise<ISessionScopeHandle> => {
|
||||
|
|
@ -353,7 +340,7 @@ async function resolveNativeSession(
|
|||
const session = await resumeById(opts.session);
|
||||
const agent = await ensureMainAgent(session);
|
||||
const profile = agent.accessor.get(IAgentProfileService);
|
||||
await applyProfileSelection(profile, opts.model);
|
||||
await applyModelOverride(profile, opts.model);
|
||||
const currentModel = profile.getModel();
|
||||
const { restorePermission } = forceAuto(agent);
|
||||
return {
|
||||
|
|
@ -372,7 +359,7 @@ async function resolveNativeSession(
|
|||
const session = await resumeById(previous.id);
|
||||
const agent = await ensureMainAgent(session);
|
||||
const profile = agent.accessor.get(IAgentProfileService);
|
||||
await applyProfileSelection(profile, opts.model);
|
||||
await applyModelOverride(profile, opts.model);
|
||||
const currentModel = profile.getModel();
|
||||
const { restorePermission } = forceAuto(agent);
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -7,11 +7,10 @@
|
|||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
import { createKimiDefaultHeaders, createKimiUserAgent, type KimiHostIdentity } from '@moonshot-ai/kimi-code-oauth';
|
||||
import { createKimiUserAgent, KIMI_CODE_PLATFORM, type KimiHostIdentity } from '@moonshot-ai/kimi-code-oauth';
|
||||
|
||||
import { CLI_USER_AGENT_PRODUCT } from '#/constant/app';
|
||||
|
||||
import { getDataDir } from '../utils/paths';
|
||||
import { KIMI_BUILD_INFO } from './build-info';
|
||||
|
||||
const MODULE_DIR = import.meta.dirname;
|
||||
|
|
@ -50,8 +49,9 @@ export function getVersion(): string {
|
|||
|
||||
export function createKimiCodeHostIdentity(version = getVersion()): KimiHostIdentity {
|
||||
return {
|
||||
userAgentProduct: CLI_USER_AGENT_PRODUCT,
|
||||
productName: CLI_USER_AGENT_PRODUCT,
|
||||
version,
|
||||
platform: KIMI_CODE_PLATFORM,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -62,10 +62,3 @@ export function createKimiCodeHostIdentity(version = getVersion()): KimiHostIden
|
|||
export function createKimiCodeUserAgent(version = getVersion()): string {
|
||||
return createKimiUserAgent(createKimiCodeHostIdentity(version));
|
||||
}
|
||||
|
||||
export function buildKimiDefaultHeaders(version: string): Record<string, string> {
|
||||
return createKimiDefaultHeaders({
|
||||
homeDir: getDataDir(),
|
||||
...createKimiCodeHostIdentity(version),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ export const CLI_UI_MODE = 'shell';
|
|||
// Telemetry ui_mode for the `kimi web` host. Same product
|
||||
// as the CLI (CLI_USER_AGENT_PRODUCT); the surface is distinguished by ui_mode.
|
||||
export const WEB_UI_MODE = 'web';
|
||||
// User-Agent suffix for the `kimi web` host: its requests go out as
|
||||
// `kimi-code-cli/<version> (web)` so upstream can tell web-UI traffic
|
||||
// apart from direct CLI runs without changing the product token or platform.
|
||||
export const WEB_USER_AGENT_SUFFIX = 'web';
|
||||
|
||||
// Give telemetry a short flush window without making CLI exit feel stuck.
|
||||
export const CLI_SHUTDOWN_TIMEOUT_MS = 3000;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import {
|
||||
effectiveModelAlias,
|
||||
SECONDARY_DERIVED_MODEL_ALIAS,
|
||||
type ExperimentalFeatureState,
|
||||
type KimiConfig,
|
||||
type ModelAlias,
|
||||
type PermissionMode,
|
||||
type Session,
|
||||
|
|
@ -250,6 +252,25 @@ export async function handleModelCommand(host: SlashCommandHost, args: string):
|
|||
showModelPicker(host, alias);
|
||||
}
|
||||
|
||||
export async function handleSecondaryModelCommand(host: SlashCommandHost, args: string): Promise<void> {
|
||||
const alias = args.trim();
|
||||
await refreshModelsForPicker(host);
|
||||
const models = pickerModelsForHost(host);
|
||||
if (Object.keys(models).length === 0) {
|
||||
host.showNotice(
|
||||
'No models configured',
|
||||
'Run /login to sign in to Kimi, or /provider to add another provider from a model catalog.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (alias.length > 0 && models[alias] === undefined) {
|
||||
host.showError(`Unknown model alias: ${alias}`);
|
||||
return;
|
||||
}
|
||||
const secondary = (await host.harness.getConfig()).secondaryModel;
|
||||
showSecondaryModelPicker(host, models, secondary?.model ?? '', secondary?.defaultEffort, alias);
|
||||
}
|
||||
|
||||
export async function handleEffortCommand(host: SlashCommandHost, args: string): Promise<void> {
|
||||
const alias = host.state.appState.model;
|
||||
const model = host.state.appState.availableModels[alias];
|
||||
|
|
@ -390,13 +411,22 @@ async function applyEditorChoice(host: SlashCommandHost, value: string): Promise
|
|||
);
|
||||
}
|
||||
|
||||
export function showModelPicker(host: SlashCommandHost, selectedValue: string = host.state.appState.model): void {
|
||||
const models = Object.fromEntries(
|
||||
Object.entries(host.state.appState.availableModels).map(([alias, model]) => [
|
||||
alias,
|
||||
effectiveModelForHost(host, model),
|
||||
]),
|
||||
/**
|
||||
* The models a picker may offer: the user's configured aliases with
|
||||
* host-effective provider resolution applied, minus the synthesized
|
||||
* `__secondary__` derived entry — a runtime artifact of the `[secondary_model]`
|
||||
* recipe that must never be selectable as a primary or secondary model.
|
||||
*/
|
||||
function pickerModelsForHost(host: SlashCommandHost): Record<string, ModelAlias> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(host.state.appState.availableModels)
|
||||
.filter(([alias]) => alias !== SECONDARY_DERIVED_MODEL_ALIAS)
|
||||
.map(([alias, model]) => [alias, effectiveModelForHost(host, model)]),
|
||||
);
|
||||
}
|
||||
|
||||
export function showModelPicker(host: SlashCommandHost, selectedValue: string = host.state.appState.model): void {
|
||||
const models = pickerModelsForHost(host);
|
||||
const entries = Object.entries(models);
|
||||
if (entries.length === 0) {
|
||||
host.showNotice(
|
||||
|
|
@ -553,6 +583,99 @@ async function persistModelSelection(
|
|||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Secondary model (`/secondary_model`)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function showSecondaryModelPicker(
|
||||
host: SlashCommandHost,
|
||||
models: Record<string, ModelAlias>,
|
||||
currentValue: string,
|
||||
currentEffort: string | undefined,
|
||||
selectedValue?: string,
|
||||
): void {
|
||||
host.mountEditorReplacement(
|
||||
new TabbedModelSelectorComponent({
|
||||
models,
|
||||
currentValue,
|
||||
selectedValue,
|
||||
currentThinkingEffort: currentEffort ?? 'off',
|
||||
title: ' Select a secondary model (subagents)',
|
||||
onSelect: ({ alias, thinking }) => {
|
||||
host.restoreEditor();
|
||||
void performSecondaryModelSwitch(host, alias, thinking);
|
||||
},
|
||||
onCancel: () => {
|
||||
host.restoreEditor();
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist-first, then live-apply: the synthesized derived entry only exists in
|
||||
* the core config after a reload. No session-only variant — a session-local
|
||||
* recipe with patch fields would bind a derived alias the core config cannot
|
||||
* resolve.
|
||||
*/
|
||||
async function performSecondaryModelSwitch(
|
||||
host: SlashCommandHost,
|
||||
alias: string,
|
||||
effort: ThinkingEffort,
|
||||
): Promise<void> {
|
||||
const displayName = modelDisplayName(alias, host.state.appState.availableModels[alias]);
|
||||
let updatedConfig: KimiConfig;
|
||||
try {
|
||||
updatedConfig = await host.harness.setConfig({
|
||||
secondaryModel: { model: alias, defaultEffort: effort },
|
||||
});
|
||||
} catch (error) {
|
||||
host.showError(`Failed to save secondary model: ${formatErrorMessage(error)}`);
|
||||
return;
|
||||
}
|
||||
if (host.session !== undefined) {
|
||||
try {
|
||||
await host.session.applyPersistedSecondaryModel();
|
||||
} catch (error) {
|
||||
host.showError(
|
||||
`Saved ${displayName} as the secondary model, but failed to apply it to this session: ${formatErrorMessage(error)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
host.setAppState({ availableModels: updatedConfig.models ?? {} });
|
||||
// Report the effective binding from the reloaded config, not the picked
|
||||
// value: KIMI_SECONDARY_MODEL / KIMI_SECONDARY_EFFORT override the recipe at
|
||||
// runtime, and the session binds the overlaid snapshot (mirrors how
|
||||
// /model displays the effective alias read back from the session).
|
||||
const effective = updatedConfig.secondaryModel;
|
||||
const envOverrides: string[] = [];
|
||||
if (effective?.model !== undefined && effective.model !== alias) {
|
||||
envOverrides.push(`KIMI_SECONDARY_MODEL=${effective.model}`);
|
||||
}
|
||||
if (effective?.defaultEffort !== undefined && effective.defaultEffort !== effort) {
|
||||
envOverrides.push(`KIMI_SECONDARY_EFFORT=${effective.defaultEffort}`);
|
||||
}
|
||||
if (envOverrides.length > 0 && effective?.model !== undefined) {
|
||||
const effectiveName = modelDisplayName(
|
||||
effective.model,
|
||||
updatedConfig.models?.[effective.model],
|
||||
);
|
||||
host.showStatus(
|
||||
`Saved ${displayName} as the secondary model, but ${envOverrides.join(' and ')} ` +
|
||||
`overrides it at runtime — subagents bind ${effectiveName} until the env var is unset.`,
|
||||
'warning',
|
||||
);
|
||||
return;
|
||||
}
|
||||
host.showStatus(
|
||||
host.session === undefined
|
||||
? `Secondary model set to ${displayName} with thinking ${effort}; applies to new sessions.`
|
||||
: `Secondary model set to ${displayName} with thinking ${effort}.`,
|
||||
'success',
|
||||
);
|
||||
}
|
||||
|
||||
function showThemePicker(host: SlashCommandHost): void {
|
||||
host.mountEditorReplacement(
|
||||
new ThemeSelectorComponent({
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import {
|
|||
handleEffortCommand,
|
||||
handleModelCommand,
|
||||
handlePlanCommand,
|
||||
handleSecondaryModelCommand,
|
||||
handleThemeCommand,
|
||||
handleYoloCommand,
|
||||
showExperimentsPanel,
|
||||
|
|
@ -71,6 +72,7 @@ export {
|
|||
handleEffortCommand,
|
||||
handleModelCommand,
|
||||
handlePlanCommand,
|
||||
handleSecondaryModelCommand,
|
||||
handleThemeCommand,
|
||||
handleYoloCommand,
|
||||
showModelPicker,
|
||||
|
|
@ -303,6 +305,9 @@ async function handleBuiltInSlashCommand(
|
|||
case 'model':
|
||||
await handleModelCommand(host, args);
|
||||
return;
|
||||
case 'secondary_model':
|
||||
await handleSecondaryModelCommand(host, args);
|
||||
return;
|
||||
case 'effort':
|
||||
await handleEffortCommand(host, args);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -184,6 +184,14 @@ export const BUILTIN_SLASH_COMMANDS = [
|
|||
priority: 100,
|
||||
availability: 'always',
|
||||
},
|
||||
{
|
||||
name: 'secondary_model',
|
||||
aliases: [],
|
||||
description: 'Configure the secondary model for subagents',
|
||||
priority: 90,
|
||||
availability: 'always',
|
||||
experimentalFlag: 'secondary-model',
|
||||
},
|
||||
{
|
||||
name: 'effort',
|
||||
aliases: ['thinking'],
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ export interface ModelSelectorOptions {
|
|||
/** Live thinking effort of the currently active model (e.g. 'off', 'on',
|
||||
* 'high'). Used to highlight the active segment for the current model. */
|
||||
readonly currentThinkingEffort: ThinkingEffort;
|
||||
/** Overrides the default ' Select a model' title line. */
|
||||
readonly title?: string;
|
||||
/** When true, typed characters filter the list (fuzzy) and a search line is shown. */
|
||||
readonly searchable?: boolean;
|
||||
/** Items per page. Lists longer than this paginate (PgUp/PgDn). */
|
||||
|
|
@ -289,7 +291,7 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|||
|
||||
const lines: string[] = [
|
||||
currentTheme.fg('primary', '─'.repeat(width)),
|
||||
currentTheme.boldFg('primary', ' Select a model') + titleSuffix,
|
||||
currentTheme.boldFg('primary', this.opts.title ?? ' Select a model') + titleSuffix,
|
||||
currentTheme.fg('textMuted', ' ' + hintParts.join(' · ')),
|
||||
];
|
||||
if (this.opts.warning !== undefined) {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@ export interface TabbedModelSelectorOptions {
|
|||
readonly currentValue: string;
|
||||
readonly selectedValue?: string;
|
||||
readonly currentThinkingEffort: string;
|
||||
/** Forwarded to each inner selector; overrides the default ' Select a model'
|
||||
* title line (e.g. the secondary-model picker). */
|
||||
readonly title?: string;
|
||||
/** When set, the tab for this provider id is initially active instead of the
|
||||
* tab derived from `currentValue`. */
|
||||
readonly initialTabId?: string;
|
||||
|
|
@ -180,6 +183,7 @@ function makeSelector(
|
|||
currentValue: opts.currentValue,
|
||||
...(selectedValue !== undefined ? { selectedValue } : {}),
|
||||
currentThinkingEffort: opts.currentThinkingEffort,
|
||||
title: opts.title,
|
||||
searchable: true,
|
||||
providerSwitchHint: true,
|
||||
warning: opts.warning,
|
||||
|
|
|
|||
|
|
@ -301,7 +301,9 @@ function formatBreakdownParts(counts: PhaseCounts): string[] {
|
|||
}
|
||||
|
||||
function formatStats(snap: ToolCallSubagentSnapshot): string {
|
||||
const parts = [`${String(snap.toolCount)} tool${snap.toolCount === 1 ? '' : 's'}`];
|
||||
const parts: string[] = [];
|
||||
if (snap.model !== undefined) parts.push(snap.model);
|
||||
parts.push(`${String(snap.toolCount)} tool${snap.toolCount === 1 ? '' : 's'}`);
|
||||
if (snap.elapsedSeconds !== undefined) parts.push(formatElapsed(snap.elapsedSeconds));
|
||||
if (snap.tokens > 0) parts.push(formatTokens(snap.tokens));
|
||||
return currentTheme.dim(` · ${parts.join(' · ')}`);
|
||||
|
|
|
|||
|
|
@ -190,6 +190,7 @@ export class AgentSwarmProgressComponent implements Component {
|
|||
private description: string;
|
||||
private readonly requestRender: (() => void) | undefined;
|
||||
private readonly availableGridHeight: (() => number | undefined) | undefined;
|
||||
private modelDisplay = '';
|
||||
private inputComplete = false;
|
||||
private failed = false;
|
||||
private aborted = false;
|
||||
|
|
@ -224,6 +225,16 @@ export class AgentSwarmProgressComponent implements Component {
|
|||
this.activitySpinnerText = provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the bound model once in the header. Every swarm member binds to the
|
||||
* same model, so the first child status update wins and later ones (e.g.
|
||||
* from resumed agents that kept a different binding) do not churn it.
|
||||
*/
|
||||
setModelDisplay(modelDisplay: string): void {
|
||||
if (this.modelDisplay.length > 0 || modelDisplay.length === 0) return;
|
||||
this.modelDisplay = modelDisplay;
|
||||
}
|
||||
|
||||
markToolCallEnded(): void {
|
||||
this.toolCallActive = false;
|
||||
this.activitySpinnerText = undefined;
|
||||
|
|
@ -481,9 +492,13 @@ export class AgentSwarmProgressComponent implements Component {
|
|||
this.description.length > 0
|
||||
? chalk.hex(this.colors.primary)(' ─ ') + chalk.hex(this.colors.text)(this.description)
|
||||
: '';
|
||||
const model =
|
||||
this.modelDisplay.length > 0
|
||||
? chalk.hex(this.colors.primary)(' ─ ') + chalk.hex(this.colors.textDim)(this.modelDisplay)
|
||||
: '';
|
||||
const prefixText = '─ ';
|
||||
const labelWidth = Math.max(1, width - visibleWidth(prefixText) - 1);
|
||||
const label = truncateToWidth(title + description, labelWidth);
|
||||
const label = truncateToWidth(title + description + model, labelWidth);
|
||||
const suffixWidth = Math.max(0, width - visibleWidth(prefixText) - visibleWidth(label));
|
||||
const suffix = suffixWidth === 0 ? '' : ` ${'─'.repeat(Math.max(0, suffixWidth - 1))}`;
|
||||
return chalk.hex(this.colors.primary)(prefixText) + label + chalk.hex(this.colors.primary)(suffix);
|
||||
|
|
|
|||
|
|
@ -93,6 +93,8 @@ export interface ToolCallSubagentSnapshot {
|
|||
readonly toolName: string;
|
||||
readonly toolCallDescription: string;
|
||||
readonly agentName: string | undefined;
|
||||
/** Display name of the model the subagent is bound to, when known (live only). */
|
||||
readonly model?: string;
|
||||
readonly phase: SubagentPhase | undefined;
|
||||
readonly toolCount: number;
|
||||
readonly elapsedSeconds: number | undefined;
|
||||
|
|
@ -595,6 +597,8 @@ export class ToolCallComponent extends Container {
|
|||
private backgroundTaskTerminalPhase: 'done' | 'failed' | undefined;
|
||||
private subagentContextTokens: number | undefined;
|
||||
private subagentUsage: TokenUsage | undefined;
|
||||
/** Display name of the model the subagent is bound to (from its `agent.status.updated`). */
|
||||
private subagentModel: string | undefined;
|
||||
private subagentResultSummary: string | undefined;
|
||||
private subagentError: string | undefined;
|
||||
private streamingProgressTimer: ReturnType<typeof setInterval> | undefined;
|
||||
|
|
@ -898,6 +902,7 @@ export class ToolCallComponent extends Container {
|
|||
toolName: this.toolCall.name,
|
||||
toolCallDescription: str(this.toolCall.args['description']) || str(this.toolCall.description),
|
||||
agentName: this.subagentAgentName,
|
||||
model: this.subagentModel,
|
||||
phase: derivedPhase,
|
||||
toolCount: finished,
|
||||
elapsedSeconds: this.getSubagentElapsedSeconds(),
|
||||
|
|
@ -1162,6 +1167,7 @@ export class ToolCallComponent extends Container {
|
|||
updateSubagentMetrics(payload: {
|
||||
contextTokens?: number | undefined;
|
||||
usage?: TokenUsage | undefined;
|
||||
modelDisplay?: string | undefined;
|
||||
}): void {
|
||||
if (payload.contextTokens !== undefined && payload.contextTokens > 0) {
|
||||
this.subagentContextTokens = payload.contextTokens;
|
||||
|
|
@ -1169,6 +1175,9 @@ export class ToolCallComponent extends Container {
|
|||
if (payload.usage !== undefined) {
|
||||
this.subagentUsage = payload.usage;
|
||||
}
|
||||
if (payload.modelDisplay !== undefined) {
|
||||
this.subagentModel = payload.modelDisplay;
|
||||
}
|
||||
this.headerText.setText(this.buildHeader());
|
||||
this.invalidate();
|
||||
this.notifySnapshotChange();
|
||||
|
|
@ -1784,9 +1793,9 @@ export class ToolCallComponent extends Container {
|
|||
}
|
||||
|
||||
private formatSingleSubagentStatsText(): string {
|
||||
const parts = [
|
||||
`${String(this.subToolActivities.size)} tool${this.subToolActivities.size === 1 ? '' : 's'}`,
|
||||
];
|
||||
const parts: string[] = [];
|
||||
if (this.subagentModel !== undefined) parts.push(this.subagentModel);
|
||||
parts.push(`${String(this.subToolActivities.size)} tool${this.subToolActivities.size === 1 ? '' : 's'}`);
|
||||
const elapsed = this.getSubagentElapsedSeconds();
|
||||
if (elapsed !== undefined) parts.push(formatElapsed(elapsed));
|
||||
const tokens =
|
||||
|
|
|
|||
|
|
@ -85,6 +85,12 @@ export class AuthFlowController {
|
|||
? 'yolo'
|
||||
: undefined,
|
||||
planMode: host.state.appState.planMode ? true : undefined,
|
||||
// The post-login session is still the startup session: carry the
|
||||
// --agent/--agent-file binding resolved at launch.
|
||||
agentProfile: host.options.startup.agentProfile,
|
||||
agentFiles: host.options.startup.agentFiles?.length
|
||||
? [...host.options.startup.agentFiles]
|
||||
: undefined,
|
||||
};
|
||||
if (host.state.appState.additionalDirs.length > 0) {
|
||||
options.additionalDirs = [...host.state.appState.additionalDirs];
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
agentSwarmDescriptionFromArgs,
|
||||
agentSwarmGridHeightForTerminalRows,
|
||||
} from '../components/messages/agent-swarm-progress';
|
||||
import { modelDisplayName } from '../components/dialogs/model-selector';
|
||||
import { MAIN_AGENT_ID } from '../constant/kimi-tui';
|
||||
import type {
|
||||
BackgroundAgentMetadata,
|
||||
|
|
@ -125,6 +126,14 @@ export class SubAgentEventHandler {
|
|||
toolCall.updateSubagentMetrics({
|
||||
contextTokens: event.contextTokens,
|
||||
usage: totalUsage,
|
||||
// The bound model alias rides every child status update (emitted right
|
||||
// after spawn); surface it on the subagent card. `modelDisplayName`
|
||||
// falls back to the alias itself when the entry is unknown (e.g. the
|
||||
// synthesized `__secondary__` derived entry is missing).
|
||||
modelDisplay:
|
||||
event.model === undefined
|
||||
? undefined
|
||||
: modelDisplayName(event.model, this.host.state.appState.availableModels[event.model]),
|
||||
});
|
||||
}
|
||||
return true;
|
||||
|
|
@ -502,6 +511,15 @@ export class SubAgentEventHandler {
|
|||
progress.appendModelDelta({ agentId: subagentId, delta: event.delta });
|
||||
} else if (event.type === 'tool.call.started') {
|
||||
progress.recordToolCall({ agentId: subagentId, toolCallId: event.toolCallId });
|
||||
} else if (event.type === 'agent.status.updated' && event.model !== undefined) {
|
||||
// The bound model alias rides every child status update (emitted right
|
||||
// after spawn). Swarm members share one binding, so the panel shows it
|
||||
// once in the header instead of per cell. `modelDisplayName` falls back
|
||||
// to the alias itself when the entry is unknown (e.g. the synthesized
|
||||
// `__secondary__` derived entry is missing).
|
||||
progress.setModelDisplay(
|
||||
modelDisplayName(event.model, this.host.state.appState.availableModels[event.model]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { writeFileSync } from 'node:fs';
|
|||
import { join } from 'node:path';
|
||||
|
||||
import type { DeviceAuthorization } from '@moonshot-ai/kimi-code-oauth';
|
||||
import { log } from '@moonshot-ai/kimi-code-sdk';
|
||||
import type {
|
||||
ApprovalRequest,
|
||||
ApprovalResponse,
|
||||
|
|
@ -174,6 +175,8 @@ export type {
|
|||
|
||||
export interface KimiTUIStartupInput {
|
||||
readonly cliOptions: CLIOptions;
|
||||
/** Profile name resolved from cliOptions --agent/--agent-file (see resolveAgentProfileSelection). */
|
||||
readonly agentProfile?: string;
|
||||
readonly additionalDirs?: readonly string[];
|
||||
readonly tuiConfig: TuiConfig;
|
||||
readonly version: string;
|
||||
|
|
@ -385,6 +388,8 @@ export class KimiTUI {
|
|||
auto: startupInput.cliOptions.auto,
|
||||
plan: startupInput.cliOptions.plan,
|
||||
model: startupInput.cliOptions.model,
|
||||
agentProfile: startupInput.agentProfile,
|
||||
agentFiles: startupInput.cliOptions.agentFiles,
|
||||
startupNotice: startupInput.startupNotice,
|
||||
},
|
||||
};
|
||||
|
|
@ -744,6 +749,10 @@ export class KimiTUI {
|
|||
model: startup.model,
|
||||
permission: startup.auto ? 'auto' : startup.yolo ? 'yolo' : undefined,
|
||||
planMode: startup.plan ? true : undefined,
|
||||
// --agent/--agent-file bind the startup session only; sessions created
|
||||
// later in this process fall back to the default profile.
|
||||
agentProfile: startup.agentProfile,
|
||||
agentFiles: startup.agentFiles?.length ? [...startup.agentFiles] : undefined,
|
||||
};
|
||||
if (this.state.appState.additionalDirs.length > 0) {
|
||||
createSessionOptions.additionalDirs = [...this.state.appState.additionalDirs];
|
||||
|
|
@ -1676,8 +1685,11 @@ export class KimiTUI {
|
|||
this.state.appState.sessionId,
|
||||
this.hasSessionContent(),
|
||||
);
|
||||
} catch {
|
||||
/* silently ignore */
|
||||
} catch (error) {
|
||||
// The picker must keep working (it renders the empty state), but a
|
||||
// swallowed failure surfaces as a misleading "No sessions found." —
|
||||
// keep a log trail so the real error stays discoverable.
|
||||
log.warn('failed to fetch sessions for picker', { error: String(error) });
|
||||
} finally {
|
||||
this.state.loadingSessions = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -244,6 +244,10 @@ export interface TUIStartupOptions {
|
|||
readonly auto: boolean;
|
||||
readonly plan: boolean;
|
||||
readonly model?: string;
|
||||
/** Resolved profile name from --agent/--agent-file; bound to the startup session only. */
|
||||
readonly agentProfile?: string;
|
||||
/** Raw --agent-file paths, passed to session creation alongside `agentProfile`. */
|
||||
readonly agentFiles?: readonly string[];
|
||||
readonly startupNotice?: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
/**
|
||||
* Scenario: top-level CLI option parsing, validation, and help discovery.
|
||||
* Responsibilities: accepted arguments map to CLIOptions and invalid combinations fail early.
|
||||
* Wiring: Commander is real; command handlers and output sinks are local test boundaries.
|
||||
* Run: pnpm -C apps/kimi-code exec vitest run test/cli/options.test.ts
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { createProgram } from '#/cli/commands';
|
||||
|
|
@ -393,6 +400,14 @@ describe('CLI options parsing', () => {
|
|||
});
|
||||
|
||||
describe('--agent / --agent-file', () => {
|
||||
it('describes agent selectors as new-session-only', () => {
|
||||
const help = createProgram('0.1.0-test', () => {}, () => {}).helpInformation();
|
||||
const normalizedHelp = help.replaceAll(/\s+/g, ' ');
|
||||
|
||||
expect(normalizedHelp).toContain('Agent profile to start the new session with.');
|
||||
expect(normalizedHelp).not.toContain('print-mode invocation');
|
||||
});
|
||||
|
||||
it('parses a single --agent', () => {
|
||||
const opts = parse(['-p', 'hi', '--agent', 'reviewer']);
|
||||
expect(opts.agent).toBe('reviewer');
|
||||
|
|
@ -437,6 +452,38 @@ describe('CLI options parsing', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('rejects --agent-file with --session', () => {
|
||||
const opts = parse(['-p', 'hi', '--agent-file', 'a.md', '--session', 'ses_123']);
|
||||
expect(() => validateOptions(opts)).toThrow(OptionConflictError);
|
||||
expect(() => validateOptions(opts)).toThrow(
|
||||
'Cannot combine --agent/--agent-file with --session/--continue',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects --agent-file with --continue', () => {
|
||||
const opts = parse(['-p', 'hi', '--agent-file', 'a.md', '--continue']);
|
||||
expect(() => validateOptions(opts)).toThrow(OptionConflictError);
|
||||
expect(() => validateOptions(opts)).toThrow(
|
||||
'Cannot combine --agent/--agent-file with --session/--continue',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects --agent with --session', () => {
|
||||
const opts = parse(['-p', 'hi', '--agent', 'reviewer', '--session', 'ses_123']);
|
||||
expect(() => validateOptions(opts)).toThrow(OptionConflictError);
|
||||
expect(() => validateOptions(opts)).toThrow(
|
||||
'Cannot combine --agent/--agent-file with --session/--continue',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects --agent with --continue in shell mode', () => {
|
||||
const opts = parse(['--agent', 'reviewer', '--continue']);
|
||||
expect(() => validateOptions(opts)).toThrow(OptionConflictError);
|
||||
expect(() => validateOptions(opts)).toThrow(
|
||||
'Cannot combine --agent/--agent-file with --session/--continue',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects empty agent values', () => {
|
||||
const opts = parse(['-p', 'hi', '--agent', ' ']);
|
||||
expect(() => validateOptions(opts)).toThrow(OptionConflictError);
|
||||
|
|
@ -449,20 +496,14 @@ describe('CLI options parsing', () => {
|
|||
expect(() => validateOptions(opts)).toThrow('Agent file path cannot be empty.');
|
||||
});
|
||||
|
||||
it('rejects the flags in shell mode', () => {
|
||||
const opts = parse(['--agent', 'reviewer']);
|
||||
expect(() => validateOptions(opts)).toThrow(OptionConflictError);
|
||||
expect(() => validateOptions(opts)).toThrow(
|
||||
'--agent/--agent-file are only available with the v2 engine',
|
||||
);
|
||||
it('accepts the flags in shell mode', () => {
|
||||
expect(validateOptions(parse(['--agent', 'reviewer']), {}).uiMode).toBe('shell');
|
||||
expect(validateOptions(parse(['--agent-file', 'a.md']), {}).uiMode).toBe('shell');
|
||||
});
|
||||
|
||||
it('rejects the flags in prompt mode without the v2 engine flag', () => {
|
||||
it('accepts the flags in prompt mode without the v2 engine flag', () => {
|
||||
const opts = parse(['-p', 'hi', '--agent-file', 'a.md']);
|
||||
expect(() => validateOptions(opts, {})).toThrow(OptionConflictError);
|
||||
expect(() => validateOptions(opts, {})).toThrow(
|
||||
'--agent/--agent-file are only available with the v2 engine',
|
||||
);
|
||||
expect(validateOptions(opts, {}).uiMode).toBe('print');
|
||||
});
|
||||
|
||||
it('accepts the flags in prompt mode with the v2 engine flag', () => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,14 @@
|
|||
/**
|
||||
* Scenario: print-mode session startup and resume routing.
|
||||
* Responsibilities: CLI options are translated into the SDK session contract and output is rendered.
|
||||
* Wiring: the SDK/telemetry/process boundaries are mocked; the print driver is real.
|
||||
* Run: pnpm -C apps/kimi-code exec vitest run test/cli/run-prompt.test.ts
|
||||
*/
|
||||
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import type { createKimiDeviceId as createKimiDeviceIdFn } from '@moonshot-ai/kimi-code-oauth';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
|
|
@ -282,6 +293,32 @@ describe('runPrompt', () => {
|
|||
expect(mocks.harnessClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('selects the profile declared by an explicit agent file for a fresh v1 session', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'kimi-run-prompt-agent-'));
|
||||
const agentFile = join(dir, 'reviewer.md');
|
||||
await writeFile(
|
||||
agentFile,
|
||||
'---\nname: reviewer\ndescription: Reviews code.\n---\n\nReview the requested change.\n',
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
try {
|
||||
await runPrompt(opts({ agentFiles: [agentFile] }), '1.2.3-test', {
|
||||
stdout: writer(),
|
||||
stderr: writer(),
|
||||
});
|
||||
|
||||
expect(mocks.harnessCreateSession).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentProfile: 'reviewer',
|
||||
agentFiles: [agentFile],
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('completes even if harness.close() never resolves (cleanup is time-bounded)', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
|
|
@ -629,6 +666,17 @@ describe('runPrompt', () => {
|
|||
expect(mocks.harnessCreateSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not forward an agent profile when resuming a concrete v1 session', async () => {
|
||||
// validateOptions rejects --agent with --session; runPrompt must not
|
||||
// forward a profile to resume even if a caller hands one over.
|
||||
await runPrompt(opts({ session: 'ses_existing', agent: 'reviewer' }), '1.2.3-test', {
|
||||
stdout: writer(),
|
||||
stderr: writer(),
|
||||
});
|
||||
|
||||
expect(mocks.harnessResumeSession).toHaveBeenCalledWith({ id: 'ses_existing' });
|
||||
});
|
||||
|
||||
it('allows resuming a concrete session when Windows workdir uses backslashes', async () => {
|
||||
const cwd = vi.spyOn(process, 'cwd').mockReturnValue(String.raw`C:\Users\kimi\project`);
|
||||
mocks.harnessListSessions.mockResolvedValueOnce([
|
||||
|
|
@ -901,6 +949,17 @@ describe('runPrompt', () => {
|
|||
expect(mocks.harnessCreateSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not forward an agent profile when continuing a previous v1 session', async () => {
|
||||
// validateOptions rejects --agent with --continue; runPrompt must not
|
||||
// forward a profile to resume even if a caller hands one over.
|
||||
await runPrompt(opts({ continue: true, agent: 'reviewer' }), '1.2.3-test', {
|
||||
stdout: writer(),
|
||||
stderr: writer(),
|
||||
});
|
||||
|
||||
expect(mocks.harnessResumeSession).toHaveBeenCalledWith({ id: 'ses_previous' });
|
||||
});
|
||||
|
||||
it('continues a previous session without a configured default model', async () => {
|
||||
mocks.harnessGetConfig.mockResolvedValueOnce({ providers: {}, telemetry: true });
|
||||
mocks.session.getStatus.mockResolvedValueOnce({ permission: 'manual', model: 'saved-model' });
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ describe('runShell', () => {
|
|||
expect(mocks.kimiHarnessConstructor).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
identity: expect.objectContaining({
|
||||
userAgentProduct: 'kimi-code-cli',
|
||||
productName: 'kimi-code-cli',
|
||||
version: '1.2.3-test',
|
||||
}),
|
||||
sessionStartedProperties: { yolo: true, auto: false, plan: true, afk: false },
|
||||
|
|
@ -319,6 +319,35 @@ describe('runShell', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('resolves the --agent profile into the TUI startup input', async () => {
|
||||
mocks.loadTuiConfig.mockResolvedValue({
|
||||
theme: 'dark',
|
||||
editorCommand: null,
|
||||
notifications: { enabled: true, condition: 'unfocused' },
|
||||
});
|
||||
mocks.tuiStart.mockResolvedValue(undefined);
|
||||
|
||||
await runShell(
|
||||
{
|
||||
session: undefined,
|
||||
continue: false,
|
||||
yolo: false,
|
||||
auto: false,
|
||||
plan: false,
|
||||
model: undefined,
|
||||
outputFormat: undefined,
|
||||
prompt: undefined,
|
||||
skillsDirs: [],
|
||||
agent: 'reviewer',
|
||||
agentFiles: [],
|
||||
},
|
||||
'1.2.3-test',
|
||||
);
|
||||
|
||||
const [, , startupInput] = mocks.kimiTuiConstructor.mock.calls[0]!;
|
||||
expect(startupInput).toMatchObject({ agentProfile: 'reviewer' });
|
||||
});
|
||||
|
||||
it('forwards skillsDirs from CLI options to the harness', async () => {
|
||||
mocks.loadTuiConfig.mockResolvedValue({
|
||||
theme: 'dark',
|
||||
|
|
|
|||
|
|
@ -205,7 +205,11 @@ function makeFakeHarness() {
|
|||
{
|
||||
platform: 'linux',
|
||||
arch: 'x64',
|
||||
clientVersion: '1.2.3-test',
|
||||
clientIdentity: {
|
||||
productName: 'test-product',
|
||||
version: '1.2.3-test',
|
||||
platform: 'test_platform',
|
||||
},
|
||||
osHomeDir: '/home/test',
|
||||
getEnv: () => undefined,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { dirname, join } from 'node:path';
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
buildKimiDefaultHeaders,
|
||||
createKimiCodeUserAgent,
|
||||
getHostPackageJsonPath,
|
||||
getHostPackageRoot,
|
||||
|
|
@ -21,12 +20,6 @@ describe('cli version helpers', () => {
|
|||
expect(getVersion()).toBe(pkg.version);
|
||||
});
|
||||
|
||||
it('builds default headers with the kimi-code-cli user-agent', () => {
|
||||
const headers = buildKimiDefaultHeaders('1.2.3');
|
||||
|
||||
expect(headers['User-Agent']).toBe('kimi-code-cli/1.2.3');
|
||||
});
|
||||
|
||||
it('builds the product user-agent for ad-hoc fetches', () => {
|
||||
expect(createKimiCodeUserAgent('1.2.3')).toBe('kimi-code-cli/1.2.3');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ describe('built-in slash command registry', () => {
|
|||
'plan',
|
||||
'reload',
|
||||
'reload-tui',
|
||||
'secondary_model',
|
||||
'sessions',
|
||||
'settings',
|
||||
'status',
|
||||
|
|
@ -188,4 +189,11 @@ describe('built-in slash command registry', () => {
|
|||
expect(resolveSlashCommandAvailability(reload!, '')).toBe('idle-only');
|
||||
expect(resolveSlashCommandAvailability(reloadTui!, '')).toBe('always');
|
||||
});
|
||||
|
||||
it('gates secondary_model behind the secondary-model experiment, always available', () => {
|
||||
const command = findBuiltInSlashCommand('secondary_model');
|
||||
expect(command).toBeDefined();
|
||||
expect((command as KimiSlashCommand).experimentalFlag).toBe('secondary-model');
|
||||
expect(resolveSlashCommandAvailability(command!, '')).toBe('always');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
230
apps/kimi-code/test/tui/commands/secondary-model.test.ts
Normal file
230
apps/kimi-code/test/tui/commands/secondary-model.test.ts
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
/**
|
||||
* Scenario: /secondary_model command behavior in the interactive TUI.
|
||||
* Responsibilities: picker filtering, persistence, live apply, and effective-model state refresh.
|
||||
* Wiring: real command and selector with the SDK/session boundaries stubbed by a small host rig.
|
||||
* Run: pnpm -C apps/kimi-code exec vitest run test/tui/commands/secondary-model.test.ts
|
||||
*/
|
||||
import type { ModelAlias, ThinkingEffort } from '@moonshot-ai/kimi-code-sdk';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { SlashCommandHost } from '#/tui/commands';
|
||||
import { handleSecondaryModelCommand } from '#/tui/commands/config';
|
||||
import { TabbedModelSelectorComponent } from '#/tui/components/dialogs/tabbed-model-selector';
|
||||
|
||||
interface PickerOptions {
|
||||
readonly models: Record<string, ModelAlias>;
|
||||
readonly currentValue: string;
|
||||
readonly currentThinkingEffort: string;
|
||||
readonly title?: string;
|
||||
readonly onSelect: (selection: { alias: string; thinking: ThinkingEffort }) => void;
|
||||
}
|
||||
|
||||
function model(name: string): ModelAlias {
|
||||
return {
|
||||
provider: 'test',
|
||||
model: name,
|
||||
maxContextSize: 200_000,
|
||||
displayName: name,
|
||||
} as unknown as ModelAlias;
|
||||
}
|
||||
|
||||
function makeHost(options?: {
|
||||
readonly withSession?: boolean;
|
||||
readonly secondaryModel?: { model: string; defaultEffort?: string };
|
||||
readonly persistedModels?: Record<string, ModelAlias>;
|
||||
/** The secondary model the reloaded config carries — env overlays win. */
|
||||
readonly effectiveSecondary?: { model: string; defaultEffort?: string };
|
||||
}) {
|
||||
const session = options?.withSession === false
|
||||
? undefined
|
||||
: { applyPersistedSecondaryModel: vi.fn(async () => {}) };
|
||||
const appState = {
|
||||
availableModels: {
|
||||
k2: model('k2'),
|
||||
cheap: model('cheap'),
|
||||
// The synthesized derived entry must never be selectable.
|
||||
'__secondary__': model('cheap'),
|
||||
} as Record<string, ModelAlias>,
|
||||
availableProviders: {},
|
||||
transcriptEntries: [],
|
||||
};
|
||||
const host = {
|
||||
state: {
|
||||
appState,
|
||||
transcriptEntries: [],
|
||||
},
|
||||
authFlow: {
|
||||
refreshOAuthProviderModels: vi.fn(async () => undefined),
|
||||
},
|
||||
harness: {
|
||||
getConfig: vi.fn(async () => ({
|
||||
providers: {},
|
||||
secondaryModel: options?.secondaryModel,
|
||||
})),
|
||||
setConfig: vi.fn(async () => ({
|
||||
providers: {},
|
||||
models: options?.persistedModels,
|
||||
secondaryModel: options?.effectiveSecondary,
|
||||
})),
|
||||
},
|
||||
session,
|
||||
setAppState: vi.fn((patch) => Object.assign(appState, patch)),
|
||||
mountEditorReplacement: vi.fn(),
|
||||
restoreEditor: vi.fn(),
|
||||
showStatus: vi.fn(),
|
||||
showError: vi.fn(),
|
||||
showNotice: vi.fn(),
|
||||
track: vi.fn(),
|
||||
} as unknown as SlashCommandHost & {
|
||||
harness: {
|
||||
getConfig: ReturnType<typeof vi.fn>;
|
||||
setConfig: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
mountEditorReplacement: ReturnType<typeof vi.fn>;
|
||||
showStatus: ReturnType<typeof vi.fn>;
|
||||
showError: ReturnType<typeof vi.fn>;
|
||||
showNotice: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
return { host, session };
|
||||
}
|
||||
|
||||
function mountedPicker(host: { mountEditorReplacement: ReturnType<typeof vi.fn> }): PickerOptions {
|
||||
expect(host.mountEditorReplacement).toHaveBeenCalledOnce();
|
||||
const component = host.mountEditorReplacement.mock.calls[0]![0];
|
||||
expect(component).toBeInstanceOf(TabbedModelSelectorComponent);
|
||||
return (component as unknown as { opts: PickerOptions }).opts;
|
||||
}
|
||||
|
||||
describe('handleSecondaryModelCommand', () => {
|
||||
it('opens the picker filtered to user models, with the configured recipe as current', async () => {
|
||||
const { host } = makeHost({ secondaryModel: { model: 'cheap', defaultEffort: 'high' } });
|
||||
|
||||
await handleSecondaryModelCommand(host, '');
|
||||
|
||||
const opts = mountedPicker(host);
|
||||
expect(Object.keys(opts.models)).toEqual(['k2', 'cheap']);
|
||||
expect(opts.currentValue).toBe('cheap');
|
||||
expect(opts.currentThinkingEffort).toBe('high');
|
||||
expect(opts.title).toContain('secondary model');
|
||||
});
|
||||
|
||||
it('persists first, then live-applies the selection to the session', async () => {
|
||||
const { host, session } = makeHost();
|
||||
|
||||
await handleSecondaryModelCommand(host, '');
|
||||
mountedPicker(host).onSelect({ alias: 'k2', thinking: 'high' });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(host.showStatus).toHaveBeenCalled();
|
||||
});
|
||||
expect(host.harness.setConfig).toHaveBeenCalledWith({
|
||||
secondaryModel: { model: 'k2', defaultEffort: 'high' },
|
||||
});
|
||||
expect(session!.applyPersistedSecondaryModel).toHaveBeenCalledWith();
|
||||
expect(host.harness.setConfig.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
session!.applyPersistedSecondaryModel.mock.invocationCallOrder[0]!,
|
||||
);
|
||||
expect(host.showError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refreshes the effective model map after a live secondary-model switch', async () => {
|
||||
const { host } = makeHost({
|
||||
persistedModels: {
|
||||
k2: model('k2'),
|
||||
cheap: model('cheap'),
|
||||
'__secondary__': model('k2'),
|
||||
},
|
||||
});
|
||||
|
||||
await handleSecondaryModelCommand(host, '');
|
||||
mountedPicker(host).onSelect({ alias: 'k2', thinking: 'high' });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(host.showStatus).toHaveBeenCalled();
|
||||
});
|
||||
expect(host.state.appState.availableModels['__secondary__']?.displayName).toBe('k2');
|
||||
});
|
||||
|
||||
it('warns with the env-overridden effective binding instead of the picked model', async () => {
|
||||
// KIMI_SECONDARY_MODEL / KIMI_SECONDARY_EFFORT win over the persisted
|
||||
// recipe: the reloaded config carries the overlaid values, and the status
|
||||
// message must name them rather than echo the pick.
|
||||
const { host } = makeHost({
|
||||
effectiveSecondary: { model: 'cheap', defaultEffort: 'low' },
|
||||
});
|
||||
|
||||
await handleSecondaryModelCommand(host, '');
|
||||
mountedPicker(host).onSelect({ alias: 'k2', thinking: 'high' });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(host.showStatus).toHaveBeenCalled();
|
||||
});
|
||||
const [message, color] = host.showStatus.mock.calls[0]!;
|
||||
expect(message).toContain('KIMI_SECONDARY_MODEL=cheap');
|
||||
expect(message).toContain('KIMI_SECONDARY_EFFORT=low');
|
||||
expect(color).toBe('warning');
|
||||
expect(host.showError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps the current effective model map when live apply fails', async () => {
|
||||
const { host, session } = makeHost({
|
||||
persistedModels: {
|
||||
k2: model('k2'),
|
||||
cheap: model('cheap'),
|
||||
'__secondary__': model('k2'),
|
||||
},
|
||||
});
|
||||
session!.applyPersistedSecondaryModel.mockRejectedValueOnce(new Error('apply failed'));
|
||||
|
||||
await handleSecondaryModelCommand(host, '');
|
||||
mountedPicker(host).onSelect({ alias: 'k2', thinking: 'high' });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(host.showError).toHaveBeenCalled();
|
||||
});
|
||||
expect(host.state.appState.availableModels['__secondary__']?.displayName).toBe('cheap');
|
||||
});
|
||||
|
||||
it('persists only when there is no session', async () => {
|
||||
const { host } = makeHost({ withSession: false });
|
||||
|
||||
await handleSecondaryModelCommand(host, '');
|
||||
mountedPicker(host).onSelect({ alias: 'k2', thinking: 'off' });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(host.showStatus).toHaveBeenCalled();
|
||||
});
|
||||
expect(host.harness.setConfig).toHaveBeenCalledWith({
|
||||
secondaryModel: { model: 'k2', defaultEffort: 'off' },
|
||||
});
|
||||
expect(host.showStatus.mock.calls[0]![0]).toContain('new sessions');
|
||||
});
|
||||
|
||||
it('rejects an unknown alias argument without opening the picker', async () => {
|
||||
const { host } = makeHost();
|
||||
|
||||
await handleSecondaryModelCommand(host, 'nope');
|
||||
|
||||
expect(host.showError).toHaveBeenCalledWith('Unknown model alias: nope');
|
||||
expect(host.mountEditorReplacement).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects the synthesized derived alias as an argument', async () => {
|
||||
const { host } = makeHost();
|
||||
|
||||
await handleSecondaryModelCommand(host, '__secondary__');
|
||||
|
||||
expect(host.showError).toHaveBeenCalledWith('Unknown model alias: __secondary__');
|
||||
expect(host.mountEditorReplacement).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows a notice when no models are configured', async () => {
|
||||
const { host } = makeHost();
|
||||
host.state.appState.availableModels = {};
|
||||
|
||||
await handleSecondaryModelCommand(host, '');
|
||||
|
||||
expect(host.showNotice).toHaveBeenCalled();
|
||||
expect(host.mountEditorReplacement).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -132,6 +132,22 @@ describe('TabbedModelSelectorComponent', () => {
|
|||
expect(hint!.indexOf('Tab toggle provider')).toBeLessThan(hint!.indexOf('↑↓ navigate'));
|
||||
});
|
||||
|
||||
it('renders the default title, and a custom title when provided', () => {
|
||||
expect(strip(make().component.render(120).join('\n'))).toContain('Select a model');
|
||||
|
||||
const titled = new TabbedModelSelectorComponent({
|
||||
models: { k2: model('Kimi K2', 'managed:kimi-code') },
|
||||
currentValue: 'k2',
|
||||
currentThinkingEffort: 'off',
|
||||
title: ' Select a secondary model (subagents)',
|
||||
onSelect: vi.fn(),
|
||||
onCancel: vi.fn(),
|
||||
});
|
||||
const out = strip(titled.render(120).join('\n'));
|
||||
expect(out).toContain('Select a secondary model (subagents)');
|
||||
expect(out).not.toContain('Select a model ');
|
||||
});
|
||||
|
||||
it('keeps the tab strip between hint and list when a warning line is present', () => {
|
||||
const component = new TabbedModelSelectorComponent({
|
||||
models: {
|
||||
|
|
|
|||
|
|
@ -91,6 +91,26 @@ describe('AgentGroupComponent', () => {
|
|||
waiting.dispose();
|
||||
});
|
||||
|
||||
it('shows the bound model in the row stats once reported', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(0);
|
||||
const ui = stubTui();
|
||||
const group = new AgentGroupComponent(ui);
|
||||
const running = createAgent('call_agent_1', 'inspect project', 'explore', ui);
|
||||
startAgent(running, 'call_agent_1', 'explore');
|
||||
|
||||
group.attach('call_agent_1', running);
|
||||
expect(renderText(group)).toContain('explore · inspect project · 0 tools');
|
||||
|
||||
running.updateSubagentMetrics({ modelDisplay: 'Kimi K2.5' });
|
||||
// Non-phase updates are throttled; flush the pending refresh.
|
||||
vi.runOnlyPendingTimers();
|
||||
expect(renderText(group)).toContain('explore · inspect project · Kimi K2.5 · 0 tools');
|
||||
|
||||
group.dispose();
|
||||
running.dispose();
|
||||
});
|
||||
|
||||
it('shows the Ctrl+B hint while agents are running and hides it once all are backgrounded', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(0);
|
||||
|
|
|
|||
|
|
@ -167,6 +167,30 @@ describe('AgentSwarmProgressComponent', () => {
|
|||
expect(output).not.toContain('01');
|
||||
});
|
||||
|
||||
it('shows the bound model display name in the header', () => {
|
||||
const component = createComponent();
|
||||
|
||||
component.setModelDisplay('kimi-k2-thinking');
|
||||
const lines = renderLines(component);
|
||||
const headerLine = lines.find((line) => line.includes('Agent Swarm'));
|
||||
|
||||
expect(headerLine).toBeDefined();
|
||||
expect(headerLine).toContain('Review changed files ─ kimi-k2-thinking');
|
||||
});
|
||||
|
||||
it('keeps the first reported model when later status updates differ', () => {
|
||||
const component = createComponent();
|
||||
|
||||
component.setModelDisplay('kimi-k2-thinking');
|
||||
component.setModelDisplay('other-model');
|
||||
component.setModelDisplay('');
|
||||
|
||||
const output = renderText(component);
|
||||
|
||||
expect(output).toContain('kimi-k2-thinking');
|
||||
expect(output).not.toContain('other-model');
|
||||
});
|
||||
|
||||
it('repaints from the active palette when the theme changes', () => {
|
||||
const previousLevel = chalk.level;
|
||||
chalk.level = 3; // force truecolor so palette differences surface as ANSI
|
||||
|
|
|
|||
|
|
@ -1033,6 +1033,34 @@ describe('ToolCallComponent', () => {
|
|||
expect(out).not.toContain('summary fallback');
|
||||
});
|
||||
|
||||
it('shows the bound model in the subagent header and group snapshot once reported', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(10_000);
|
||||
const component = new ToolCallComponent(
|
||||
{
|
||||
id: 'call_agent_model',
|
||||
name: 'Agent',
|
||||
args: { description: 'explore project' },
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
component.onSubagentSpawned({
|
||||
agentId: 'sub_model_1',
|
||||
agentName: 'explore',
|
||||
runInBackground: false,
|
||||
});
|
||||
|
||||
let out = strip(component.render(120).join('\n'));
|
||||
expect(out).toContain('Explore Agent Queued (explore project) · 0 tools');
|
||||
expect(out).not.toContain('Kimi K2.5');
|
||||
|
||||
component.updateSubagentMetrics({ modelDisplay: 'Kimi K2.5' });
|
||||
|
||||
out = strip(component.render(120).join('\n'));
|
||||
expect(out).toContain('Explore Agent Queued (explore project) · Kimi K2.5 · 0 tools');
|
||||
expect(component.getSubagentSnapshot().model).toBe('Kimi K2.5');
|
||||
});
|
||||
|
||||
it('shows Backgrounded after a foreground subagent is detached, even after setResult', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(0);
|
||||
|
|
|
|||
|
|
@ -281,6 +281,24 @@ describe('KimiTUI startup', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('binds the resolved agent profile and agent files to the startup session', async () => {
|
||||
const session = makeSession();
|
||||
const harness = makeHarness(session);
|
||||
const driver = makeDriver(harness, {
|
||||
...makeStartupInput({ agent: 'reviewer', agentFiles: ['reviewer.md'] }),
|
||||
agentProfile: 'reviewer',
|
||||
});
|
||||
|
||||
await expect(driver.init()).resolves.toBe(false);
|
||||
|
||||
expect(harness.createSession).toHaveBeenCalledWith({
|
||||
workDir: '/tmp/proj-a',
|
||||
agentProfile: 'reviewer',
|
||||
agentFiles: ['reviewer.md'],
|
||||
});
|
||||
expect(driver.state.startupState).toBe('ready');
|
||||
});
|
||||
|
||||
it('resumes the latest session for --continue and marks history for replay', async () => {
|
||||
const session = makeSession({ id: 'ses-latest' });
|
||||
const harness = makeHarness(session, {
|
||||
|
|
@ -1205,6 +1223,43 @@ describe('KimiTUI startup', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('carries the agent binding into the post-login startup session', async () => {
|
||||
const session = makeSession();
|
||||
const createSession = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(loginRequiredError())
|
||||
.mockResolvedValueOnce(session);
|
||||
const harness = makeHarness(session, {
|
||||
getConfig: vi.fn(async () => ({
|
||||
defaultModel: 'k2',
|
||||
thinking: { enabled: false },
|
||||
models: {
|
||||
k2: { model: 'moonshot-v1', maxContextSize: 100 },
|
||||
},
|
||||
})),
|
||||
createSession,
|
||||
});
|
||||
const driver = makeDriver(harness, {
|
||||
...makeStartupInput({ agent: 'reviewer', agentFiles: ['reviewer.md'] }),
|
||||
agentProfile: 'reviewer',
|
||||
});
|
||||
|
||||
await expect(driver.init()).resolves.toBe(false);
|
||||
|
||||
vi.mocked(promptPlatformSelection).mockResolvedValue('kimi-code');
|
||||
await handleLoginCommand(driver as any);
|
||||
|
||||
expect(createSession).toHaveBeenNthCalledWith(2, {
|
||||
workDir: '/tmp/proj-a',
|
||||
model: 'k2',
|
||||
thinking: 'off',
|
||||
permission: undefined,
|
||||
planMode: undefined,
|
||||
agentProfile: 'reviewer',
|
||||
agentFiles: ['reviewer.md'],
|
||||
});
|
||||
});
|
||||
|
||||
it('does not force manual permission after OAuth login without --yolo', async () => {
|
||||
const session = makeSession({
|
||||
getStatus: vi.fn(async () => ({
|
||||
|
|
|
|||
|
|
@ -1,9 +1,20 @@
|
|||
# Changelog
|
||||
|
||||
## 0.6.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#2393](https://github.com/MoonshotAI/kimi-code/pull/2393) [`6d0a046`](https://github.com/MoonshotAI/kimi-code/commit/6d0a046488edda56219961b253c4787abae7a113) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Fix new users getting stranded on "Model setup required" with no way back to sign-in when the first login finishes authorization but fails to complete model setup; the screen now offers a path back to the sign-in page so login can be retried.
|
||||
- [#2402](https://github.com/MoonshotAI/kimi-code/pull/2402) [`0f3b106`](https://github.com/MoonshotAI/kimi-code/commit/0f3b106c4260ad626f66bc5c457a535d3163f2bc) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Reword the sign-in waiting message from "Waiting for authorization" to "Waiting for authentication".
|
||||
|
||||
- Updated dependencies [[`40172c7`](https://github.com/MoonshotAI/kimi-code/commit/40172c7ca96ca981b043b793588dd32e898979fa)]:
|
||||
- @moonshot-ai/kimi-code-sdk@0.15.0
|
||||
|
||||
## 0.6.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1994](https://github.com/MoonshotAI/kimi-code/pull/1994) [`beeb964`](https://github.com/MoonshotAI/kimi-code/commit/beeb964393c8f9a38c2b1e2273e4415fc434b16d) Thanks [@RealKai42](https://github.com/RealKai42)! - Reduce webview streaming re-render churn: settled assistant messages no longer re-render on every streaming delta, and local images over 10MB are no longer inlined into the webview DOM.
|
||||
- Updated dependencies [[`ec88d35`](https://github.com/MoonshotAI/kimi-code/commit/ec88d352e8f4dc5e8ffd1212f016138458f69893), [`b5efba7`](https://github.com/MoonshotAI/kimi-code/commit/b5efba7abcaf4041f81ec520097a61e6546e8c50), [`ce0e3ce`](https://github.com/MoonshotAI/kimi-code/commit/ce0e3ceb04223bdaad8e8931bad46eff561055b6), [`e458323`](https://github.com/MoonshotAI/kimi-code/commit/e45832398d0d9cad98dbad1cbf1e5b103a20aace)]:
|
||||
- @moonshot-ai/kimi-code-sdk@0.14.0
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ stay in the trusted Extension Host.
|
|||
|
||||
The runtime constructs the SDK client with:
|
||||
|
||||
- `userAgentProduct: "kimi-code-vscode"`
|
||||
- `productName: "kimi-code-vscode"`
|
||||
- `version` from `apps/vscode/package.json`
|
||||
- `uiMode: "vscode"`
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"publisher": "moonshot-ai",
|
||||
"displayName": "Kimi Code",
|
||||
"description": "Official Kimi Code plugin for VS Code",
|
||||
"version": "0.6.5",
|
||||
"version": "0.6.6",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -297,6 +297,9 @@ function walkSyntax(value, visit) {
|
|||
function isRuntimeRequire(callee) {
|
||||
if (callee?.type === 'Identifier') return /^(?:__)?require\d*$/.test(callee.name);
|
||||
if (callee?.type !== 'MemberExpression' || callee.computed === true) return false;
|
||||
// `this.require(...)` is an ordinary class method call (e.g. a private field
|
||||
// accessor in bundled sources), never a CommonJS require of a bare specifier.
|
||||
if (callee.object?.type === 'ThisExpression') return false;
|
||||
return callee.property?.type === 'Identifier' && callee.property.name === 'require';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,8 +60,9 @@ export class KimiRuntime {
|
|||
createKimiHarness({
|
||||
...(options.homeDir === undefined ? {} : { homeDir: options.homeDir }),
|
||||
identity: {
|
||||
userAgentProduct: "kimi-code-vscode",
|
||||
productName: "kimi-code-vscode",
|
||||
version: options.version,
|
||||
platform: "kimi_code_vscode",
|
||||
},
|
||||
uiMode: "vscode",
|
||||
});
|
||||
|
|
|
|||
86
apps/vscode/test/app-init.test.ts
Normal file
86
apps/vscode/test/app-init.test.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* Scenario: App-level view routing after init, across login state transitions.
|
||||
* Responsibilities: the sign-in screen must stay reachable from every state — in
|
||||
* particular the no-models state (a managed OAuth token exists but config.toml
|
||||
* has no models, e.g. a first login whose model provisioning failed after the
|
||||
* device flow already persisted the token), where Reload alone can never change
|
||||
* the on-disk state and the user would otherwise be stranded.
|
||||
* Wiring: resolveAppView is pure; the bridge and toast boundaries are mocked away.
|
||||
* Run: pnpm exec vitest run --config apps/vscode/vitest.config.ts test/app-init.test.ts
|
||||
*/
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/services", () => ({
|
||||
bridge: {},
|
||||
Events: {},
|
||||
}));
|
||||
vi.mock("@/components/ui/sonner", () => ({
|
||||
toast: { error: vi.fn(), warning: vi.fn() },
|
||||
}));
|
||||
|
||||
import { resolveAppView, type AppStatus } from "../webview-ui/src/hooks/useAppInit";
|
||||
|
||||
function resolve(
|
||||
status: AppStatus,
|
||||
options: { modelsCount?: number; skippedLogin?: boolean; showLogin?: boolean } = {},
|
||||
) {
|
||||
return resolveAppView({
|
||||
status,
|
||||
modelsCount: options.modelsCount ?? 0,
|
||||
skippedLogin: options.skippedLogin ?? false,
|
||||
showLogin: options.showLogin ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
describe("resolveAppView", () => {
|
||||
it("routes a brand-new user (no token, no models) to the login screen", () => {
|
||||
expect(resolve("not-logged-in")).toEqual({ view: "login" });
|
||||
});
|
||||
|
||||
it("routes a skipped login without models to no-models with a sign-in path", () => {
|
||||
expect(resolve("not-logged-in", { skippedLogin: true })).toEqual({
|
||||
view: "status",
|
||||
status: "no-models",
|
||||
canGoToLogin: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("routes a skipped login with models to the main view", () => {
|
||||
expect(resolve("not-logged-in", { skippedLogin: true, modelsCount: 2 })).toEqual({
|
||||
view: "main",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a sign-in path in the no-models trap (token without model config)", () => {
|
||||
// Regression: this state previously rendered "Model setup required" with only
|
||||
// a Reload button, making the login screen unreachable for affected users.
|
||||
expect(resolve("no-models")).toEqual({
|
||||
view: "status",
|
||||
status: "no-models",
|
||||
canGoToLogin: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("routes to the login screen when the user asks for it from any state", () => {
|
||||
expect(resolve("no-models", { showLogin: true })).toEqual({ view: "login" });
|
||||
expect(resolve("ready", { showLogin: true, modelsCount: 1 })).toEqual({ view: "login" });
|
||||
});
|
||||
|
||||
it("routes a no-models user who skips again back to no-models with a sign-in path", () => {
|
||||
expect(resolve("no-models", { skippedLogin: true })).toEqual({
|
||||
view: "status",
|
||||
status: "no-models",
|
||||
canGoToLogin: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("routes ready to the main view", () => {
|
||||
expect(resolve("ready", { modelsCount: 1 })).toEqual({ view: "main" });
|
||||
});
|
||||
|
||||
it("routes non-login error statuses to status screens without a sign-in path", () => {
|
||||
for (const status of ["loading", "no-workspace", "runtime-error"] as const) {
|
||||
expect(resolve(status)).toEqual({ view: "status", status, canGoToLogin: false });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -141,7 +141,7 @@ async function createRuntimeRig(extraAliases: readonly string[] = []): Promise<R
|
|||
async function createPlainHarness(homeDir: string): Promise<KimiHarness> {
|
||||
const harness = createKimiHarness({
|
||||
homeDir,
|
||||
identity: { userAgentProduct: "kimi-code-cli", version: "test" },
|
||||
identity: { productName: "kimi-code-cli", version: "test", platform: "kimi_code_cli" },
|
||||
});
|
||||
cleanups.push(() => harness.close());
|
||||
return harness;
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ async function createReplayRig(): Promise<ReplayRig> {
|
|||
const provider = await createFakeProviderHarness();
|
||||
const harness = createKimiHarness({
|
||||
homeDir,
|
||||
identity: { userAgentProduct: "kimi-code-vscode", version: "test" },
|
||||
identity: { productName: "kimi-code-vscode", version: "test", platform: "kimi_code_vscode" },
|
||||
});
|
||||
await harness.setConfig({
|
||||
providers: {
|
||||
|
|
|
|||
|
|
@ -128,6 +128,27 @@ describe('VSIX verifier CLI (package contract and failure details)', () => {
|
|||
expect(result.stderr).toContain('Bare runtime dependency "left-pad"');
|
||||
});
|
||||
|
||||
it('does not mistake a bundled this.require(...) method call for a runtime dependency', async () => {
|
||||
const fixture = await makeVsixFixture('darwin-arm64');
|
||||
await writeFile(
|
||||
join(fixture, 'extension', 'dist', 'extension.js'),
|
||||
'class HostEnvironment {\n' +
|
||||
' require(field) { return this.info[field]; }\n' +
|
||||
' get osKind() { return this.require("osKind"); }\n' +
|
||||
'}\n' +
|
||||
'export function activate() { return new HostEnvironment(); }\n',
|
||||
);
|
||||
|
||||
const result = runNode(verifierScript, [
|
||||
'--target',
|
||||
'darwin-arm64',
|
||||
'--directory',
|
||||
fixture,
|
||||
]);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
});
|
||||
|
||||
it('rejects generated session state inside the package', async () => {
|
||||
const fixture = await makeVsixFixture('win32-arm64');
|
||||
const stateDir = join(fixture, 'extension', 'runtime', 'profile');
|
||||
|
|
|
|||
|
|
@ -15,5 +15,5 @@
|
|||
}
|
||||
},
|
||||
"include": ["src/**/*", "shared/**/*", "test/**/*"],
|
||||
"exclude": ["dist", "node_modules", "webview-ui", "test/settings-store.test.ts"]
|
||||
"exclude": ["dist", "node_modules", "webview-ui", "test/settings-store.test.ts", "test/app-init.test.ts"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { LoginScreen } from "./components/LoginScreen";
|
|||
import { Toaster, toast } from "./components/ui/sonner";
|
||||
import { useChatStore, useSettingsStore } from "./stores";
|
||||
import { bridge, Events } from "./services";
|
||||
import { useAppInit } from "./hooks/useAppInit";
|
||||
import { useAppInit, resolveAppView } from "./hooks/useAppInit";
|
||||
import { isPreflightError } from "shared/errors";
|
||||
import type { UIStreamEvent, StreamError, ExtensionConfig } from "shared/types";
|
||||
import "./styles/index.css";
|
||||
|
|
@ -81,22 +81,34 @@ function MainContent({ onAuthAction }: { onAuthAction: () => void }) {
|
|||
export default function App() {
|
||||
const { status, errorMessage, modelsCount, refresh } = useAppInit();
|
||||
const [skippedLogin, setSkippedLogin] = useState(false);
|
||||
const [showLogin, setShowLogin] = useState(false);
|
||||
|
||||
const handleLoginSuccess = useCallback(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const handleSkip = useCallback(() => {
|
||||
setSkippedLogin(true);
|
||||
}, []);
|
||||
|
||||
const handleAuthAction = useCallback(() => {
|
||||
setShowLogin(false);
|
||||
setSkippedLogin(false);
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
// 未登录且未跳过
|
||||
if (status === "not-logged-in" && !skippedLogin) {
|
||||
const handleSkip = useCallback(() => {
|
||||
setShowLogin(false);
|
||||
setSkippedLogin(true);
|
||||
}, []);
|
||||
|
||||
const handleShowLogin = useCallback(() => {
|
||||
setSkippedLogin(false);
|
||||
setShowLogin(true);
|
||||
}, []);
|
||||
|
||||
const handleAuthAction = useCallback(() => {
|
||||
setSkippedLogin(false);
|
||||
setShowLogin(false);
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const resolution = resolveAppView({ status, modelsCount, skippedLogin, showLogin });
|
||||
|
||||
// 登录界面:未登录且未跳过,或用户从其他界面主动选择登录
|
||||
if (resolution.view === "login") {
|
||||
return (
|
||||
<div className="flex flex-col h-screen text-foreground overflow-hidden">
|
||||
<Header />
|
||||
|
|
@ -106,23 +118,17 @@ export default function App() {
|
|||
);
|
||||
}
|
||||
|
||||
// 跳过登录但没有模型
|
||||
if (skippedLogin && modelsCount === 0) {
|
||||
// 错误与设置状态界面;no-models 必须保留回到登录界面的入口
|
||||
if (resolution.view === "status") {
|
||||
return (
|
||||
<div className="flex flex-col h-screen text-foreground overflow-hidden">
|
||||
<Header />
|
||||
<ConfigErrorScreen type="no-models" errorMessage={errorMessage} onRefresh={refresh} onBackToLogin={() => setSkippedLogin(false)} />
|
||||
<Toaster position="top-center" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 其他错误状态
|
||||
if (status !== "ready" && status !== "not-logged-in") {
|
||||
return (
|
||||
<div className="flex flex-col h-screen text-foreground overflow-hidden">
|
||||
<Header />
|
||||
<ConfigErrorScreen type={status} errorMessage={errorMessage} onRefresh={refresh} />
|
||||
<ConfigErrorScreen
|
||||
type={resolution.status}
|
||||
errorMessage={errorMessage}
|
||||
onRefresh={refresh}
|
||||
onBackToLogin={resolution.canGoToLogin ? handleShowLogin : undefined}
|
||||
/>
|
||||
<Toaster position="top-center" />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export function LoginScreen({ onLoginSuccess, onSkip }: LoginScreenProps) {
|
|||
<div className="space-y-2">
|
||||
<div className="inline-flex items-center gap-2 text-blue-500">
|
||||
<IconLoader2 className="size-5 animate-spin" />
|
||||
<span className="text-sm font-medium">Waiting for authorization...</span>
|
||||
<span className="text-sm font-medium">Waiting for authentication...</span>
|
||||
</div>
|
||||
<p className="text-xs leading-5 text-muted-foreground text-left">A browser window should open automatically. Complete the sign-in process there.</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,45 @@ import type { ExtensionConfig } from "shared/types";
|
|||
|
||||
export type AppStatus = "loading" | "no-workspace" | "runtime-error" | "not-logged-in" | "no-models" | "ready";
|
||||
|
||||
export type ConfigErrorStatus = "loading" | "no-workspace" | "runtime-error" | "no-models";
|
||||
|
||||
export type AppViewResolution =
|
||||
| { readonly view: "login" }
|
||||
| {
|
||||
readonly view: "status";
|
||||
readonly status: ConfigErrorStatus;
|
||||
/** True when the status screen must offer a path to the sign-in screen. */
|
||||
readonly canGoToLogin: boolean;
|
||||
}
|
||||
| { readonly view: "main" };
|
||||
|
||||
/**
|
||||
* Pure view router for App. The `no-models` status (a managed OAuth token
|
||||
* exists but config.toml has no models — e.g. a first login whose model
|
||||
* provisioning failed after the device flow already persisted the token)
|
||||
* must always keep a path back to the sign-in screen: Reload alone cannot
|
||||
* change the on-disk state, so without it the user is stranded and the
|
||||
* login UI becomes unreachable.
|
||||
*/
|
||||
export function resolveAppView(input: {
|
||||
readonly status: AppStatus;
|
||||
readonly modelsCount: number;
|
||||
readonly skippedLogin: boolean;
|
||||
readonly showLogin: boolean;
|
||||
}): AppViewResolution {
|
||||
const { status, modelsCount, skippedLogin, showLogin } = input;
|
||||
if (showLogin || (status === "not-logged-in" && !skippedLogin)) {
|
||||
return { view: "login" };
|
||||
}
|
||||
if (skippedLogin && modelsCount === 0) {
|
||||
return { view: "status", status: "no-models", canGoToLogin: true };
|
||||
}
|
||||
if (status !== "ready" && status !== "not-logged-in") {
|
||||
return { view: "status", status, canGoToLogin: status === "no-models" };
|
||||
}
|
||||
return { view: "main" };
|
||||
}
|
||||
|
||||
export interface AppInitState {
|
||||
status: AppStatus;
|
||||
errorMessage: string | null;
|
||||
|
|
|
|||
|
|
@ -20,5 +20,5 @@
|
|||
"shared/*": ["../shared/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "../test/settings-store.test.ts"]
|
||||
"include": ["src", "../test/settings-store.test.ts", "../test/app-init.test.ts"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ Fields in the config file fall into two categories: **top-level scalars** that d
|
|||
| `providers` | `table` | `{}` | API provider table → [`providers`](#providers) |
|
||||
| `models` | `table` | — | Model alias table → [`models`](#models) |
|
||||
| `thinking` | `table` | — | Default parameters for Thinking mode → [`thinking`](#thinking) |
|
||||
| `loop_control` | `table` | — | Agent loop control parameters → [`loop_control`](#loop_control) |
|
||||
| `loop_control` | `table` | — | Agent loop control parameters → [`loop_control`](#loop-control) |
|
||||
| `background` | `table` | — | Background task runtime parameters → [`background`](#background) |
|
||||
| `tools` | `table` | — | Global tool switch → [`tools`](#tools) |
|
||||
| `image` | `table` | — | Image compression parameters → [`image`](#image) |
|
||||
|
|
@ -186,13 +186,15 @@ display_name = "Kimi for Coding (custom)"
|
|||
|
||||
`[models."<alias>".overrides]` accepts ordinary model fields such as `max_context_size`, `max_input_size`, `max_output_size`, `capabilities`, `display_name`, `reasoning_key`, `adaptive_thinking`, `support_efforts`, `default_effort`, and `off_effort`. It does not accept identity / routing fields: `provider`, `model`, `protocol`, `beta_api`, and `base_url`.
|
||||
|
||||
You can also switch models temporarily without touching the config file — by setting `KIMI_MODEL_*` environment variables, the CLI synthesizes a temporary provider in memory that does not persist after restart. See [Define a model from environment variables](./env-vars.md#define-a-model-from-environment-variables-kimi_model).
|
||||
You can also switch models temporarily without touching the config file — by setting `KIMI_MODEL_*` environment variables, the CLI synthesizes a temporary provider in memory that does not persist after restart. See [Define a model from environment variables](./env-vars.md#define-a-model-from-environment-variables-kimi-model).
|
||||
|
||||
## `secondary_model`
|
||||
|
||||
The secondary model is a second model pointer next to the primary `default_model` — typically a cheaper model that features can bind to when they do not need the main model. Its consumer today is subagent spawning: when set, newly spawned subagents (`Agent` / `AgentSwarm`) bind to it by default instead of inheriting the main agent's model, and the main agent is told it can pick per spawn between `"secondary"` (this model) and `"primary"` (the main model). When unset, subagents inherit the main agent's model.
|
||||
|
||||
This feature is experimental and disabled by default. Under `kimi web`, enable it with `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL=1`. Under `kimi -p`, `KIMI_CODE_EXPERIMENTAL_FLAG=1` is already required to select the v2 engine and also enables this feature. The interactive TUI ignores the configuration.
|
||||
This feature is experimental and disabled by default. Enable it with `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL=1`, or the master `KIMI_CODE_EXPERIMENTAL_FLAG=1`. It takes effect in every launch mode, including the interactive TUI.
|
||||
|
||||
In the interactive TUI, the [`/secondary_model`](../reference/slash-commands.md) command opens a model picker that writes this section and live-applies it to the current session, so newly spawned subagents bind the new secondary model right away.
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
|
|
@ -384,7 +386,7 @@ Alongside `config.toml`, the CLI keeps terminal-UI and client preferences in a c
|
|||
|
||||
| Field | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `theme` | `string` | `auto` | Color theme: `auto` (follow the terminal), `dark`, `light`, or the name of a [custom theme](../customization/themes) |
|
||||
| `theme` | `string` | `auto` | Color theme: `auto` (follow the terminal), `dark`, `light`, or the name of a [custom theme](../customization/themes.md) |
|
||||
| `disable_paste_burst` | `boolean` | `false` | Disable the non-bracketed paste-burst fallback that keeps rapid multi-line pastes from submitting line by line |
|
||||
| `[editor].command` | `string` | `""` | External editor command for composing long input; empty falls back to `$VISUAL` / `$EDITOR` |
|
||||
| `[notifications].enabled` | `boolean` | `true` | Whether desktop notifications are sent |
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export KIMI_DISABLE_TELEMETRY=1
|
|||
|
||||
### `KIMI_MODEL_*` family
|
||||
|
||||
Switch models temporarily without modifying `config.toml` — when `KIMI_MODEL_NAME` is set, the CLI synthesizes a temporary provider in memory; the change does not persist after restart. See [Define a model from environment variables](#define-a-model-from-environment-variables-kimi_model).
|
||||
Switch models temporarily without modifying `config.toml` — when `KIMI_MODEL_NAME` is set, the CLI synthesizes a temporary provider in memory; the change does not persist after restart. See [Define a model from environment variables](#define-a-model-from-environment-variables-kimi-model).
|
||||
|
||||
## Provider credential key names (written in config.toml)
|
||||
|
||||
|
|
@ -128,9 +128,9 @@ Switches that control the behavior of subsystems such as telemetry, background t
|
|||
| `KIMI_CODE_PLUGIN_MARKETPLACE_URL` | Override the plugin marketplace JSON loaded by `/plugins`; useful for dev loopback servers, staging CDN files, or alternate marketplace directories | `https://code.kimi.com/kimi-code/plugins/marketplace.json`; also accepts `http://`, `file://` URLs, and local paths |
|
||||
| `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` | Cap how many AgentSwarm subagents run concurrently during the initial ramp; leave unset for no cap | Positive integer; invalid values fail fast |
|
||||
| `KIMI_SUBAGENT_TIMEOUT_MS` | Maximum wall-clock time (ms) a single subagent (`Agent` / `AgentSwarm`) may run; takes higher priority than `[subagent] timeout_ms` in `config.toml` (default `7200000`, i.e. 2 hours) | Positive integer; invalid values fall back to the config or default |
|
||||
| `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL` | Enable experimental secondary-model behavior under `kimi web`; `kimi -p` still requires `KIMI_CODE_EXPERIMENTAL_FLAG=1` to select the v2 engine, which also enables this feature | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` |
|
||||
| `KIMI_SECONDARY_MODEL` | Secondary model; takes higher priority than `[secondary_model] model` in `config.toml`. When the secondary-model experiment is enabled, newly spawned subagents (`Agent` / `AgentSwarm`) bind to it by default instead of inheriting the main agent's model (not supported in the TUI) | A model id from your configured `[models]`, e.g. `kimi-code/kimi-k2.5`; blank values are ignored |
|
||||
| `KIMI_SECONDARY_EFFORT` | Thinking effort for the secondary model; takes higher priority than `[secondary_model] default_effort` in `config.toml` and applies only when both the model and its experiment are enabled (not supported in the TUI) | An effort value, e.g. `low`; blank values are ignored |
|
||||
| `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL` | Enable the experimental secondary-model feature in every launch mode, including the interactive TUI; the master `KIMI_CODE_EXPERIMENTAL_FLAG=1` also enables it | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` |
|
||||
| `KIMI_SECONDARY_MODEL` | Secondary model; takes higher priority than `[secondary_model] model` in `config.toml`. When the secondary-model experiment is enabled, newly spawned subagents (`Agent` / `AgentSwarm`) bind to it by default instead of inheriting the main agent's model | A model id from your configured `[models]`, e.g. `kimi-code/kimi-k2.5`; blank values are ignored |
|
||||
| `KIMI_SECONDARY_EFFORT` | Thinking effort for the secondary model; takes higher priority than `[secondary_model] default_effort` in `config.toml` and applies only when both the model and its experiment are enabled | An effort value, e.g. `low`; blank values are ignored |
|
||||
| `KIMI_MCP_STARTUP_TIMEOUT_MS` | Global default connection timeout (ms) for all MCP servers; takes higher priority than `[mcp] startup_timeout_ms` in `config.toml`, but a per-server `startupTimeoutMs` in `mcp.json` still wins (default `30000`) | Integer from `1` to `2147483647`; invalid values are ignored |
|
||||
| `KIMI_MCP_TOOL_TIMEOUT_MS` | Global default single tool-call timeout (ms) for all MCP servers; takes higher priority than `[mcp] tool_timeout_ms` in `config.toml`, but a per-server `toolTimeoutMs` in `mcp.json` still wins (default `60000`) | Integer from `1` to `2147483647`; invalid values are ignored |
|
||||
| `KIMI_LOOP_MAX_STEPS_PER_TURN` | Maximum Agent steps per turn; takes higher priority than `[loop_control] max_steps_per_turn` in `config.toml` (unset or `0` means unlimited) | Non-negative integer; invalid values are ignored |
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ For a single provider, credentials are resolved in this order:
|
|||
|
||||
> The `[providers.<name>.env]` sub-table is just a TOML section in the config file — it does not write anything into the shell environment. It is only consulted when the corresponding direct field (`api_key` / `base_url`) is empty.
|
||||
|
||||
For the full list of credential key names, see [Environment variables: provider credential key names](./env-vars.md#provider-credential-key-names-written-in-configtoml).
|
||||
For the full list of credential key names, see [Environment variables: provider credential key names](./env-vars.md#provider-credential-key-names-written-in-config-toml).
|
||||
|
||||
## Command-line options
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ Beyond the three built-in sub-agents, you can define your own agents as Markdown
|
|||
|
||||
### Agent Locations
|
||||
|
||||
Kimi Code CLI discovers agent files by scope; more specific scopes take higher priority: **Explicit (`--agent-file`) > Project > Extra > User > Built-in**. When two files define the same `name`, the higher-priority scope wins. Each directory is scanned recursively for `.md` files.
|
||||
Kimi Code CLI discovers agent files by scope; more specific scopes take higher priority: **Explicit (`--agent-file`) > Project > Extra > User > Plugin > Built-in**. When two files define the same `name`, the higher-priority scope wins. Each directory is scanned recursively for `.md` files.
|
||||
|
||||
**User level** (applies to all projects):
|
||||
- `$KIMI_CODE_HOME/agents/` (default: `~/.kimi-code/agents/`)
|
||||
|
|
@ -63,6 +63,8 @@ The Kimi-specific user agent directory moves with `KIMI_CODE_HOME`, while the ge
|
|||
extra_agent_dirs = ["~/team-agents", ".agents/team-agents"]
|
||||
```
|
||||
|
||||
**Plugin level**: directories declared in an enabled plugin's manifest `agents` field (when omitted, the `agents/` directory under the plugin root is picked up automatically); see [Plugin Agents](./plugins.md#plugin-agents). Plugin agents outrank only the built-in agents.
|
||||
|
||||
**Built-in agents** are distributed with the CLI and have the lowest priority. A directory-discovered file does not override a same-name built-in Agent unless its frontmatter declares `override: true`. A file loaded through `--agent-file` is treated as explicit launch intent, may override a same-name built-in Agent, outranks every directory scope, and applies to the current launch only. Separately, `$KIMI_CODE_HOME/SYSTEM.md` permanently overrides the default main agent's system prompt (it is not part of agent-file discovery); its precedence interactions are covered in the SYSTEM.md section below.
|
||||
|
||||
::: warning Trust model
|
||||
|
|
@ -105,11 +107,11 @@ You are a strict code reviewer. Read the diff, then report findings grouped by s
|
|||
|
||||
Built-in and user tools match by exact, case-sensitive name; entries starting with `mcp__` match MCP tools as globs. Three entry shapes never match anything and are reported with a warning when the profile takes effect: a wildcard outside an `mcp__` pattern (a bare `*` in `disallowedTools` disables nothing), an `mcp__` literal that is not a full `mcp__<server>__<tool>` name (`mcp__github` matches nothing — use `mcp__github__*` for the whole server), and a name no registered or built-in tool has (usually a typo, such as `read` instead of `Read`).
|
||||
|
||||
The body is the agent's system prompt, and it is rendered as a template each time the prompt is built: `${var}` placeholders substitute live context values — unknown variables stay verbatim, a bare `$` is never special, and a variable with no context value renders as an empty string. `${base_prompt}` embeds the effective default system prompt (the built-in default, or your `SYSTEM.md` override when present), so a file can wrap the default behavior instead of replacing it. The available variables are listed in the SYSTEM.md section below.
|
||||
The body is the agent's system prompt, and it is rendered as a template each time the prompt is built: `${var}` placeholders substitute live context values — unknown variables stay verbatim, a bare `$` is never special, and a variable with no context value renders as an empty string. `${base_prompt}` embeds the effective default system prompt (the built-in default, or your `SYSTEM.md` override when present), so a file can wrap the default behavior instead of replacing it. If the file replaces the default prompt but should still honor instructions contributed by enabled plugins, place `${plugin_sections}` where those instructions should appear. The available variables are listed in the SYSTEM.md section below.
|
||||
|
||||
Unknown fields are ignored, so newer files stay readable by older versions. Fields from other agent tools (such as Claude Code's `model` or OpenCode's `mode`) are ignored the same way, the comma-separated `tools` form keeps Claude Code-style agent files loadable, and a missing `name` falls back to the file name so OpenCode-style files load too — a minimal file with `description` and a body works across tools.
|
||||
|
||||
`model_preference` applies only to newly spawned subagents when the secondary-model experiment is enabled. Under `kimi web`, set `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL=1`; under experimental `kimi -p`, the required `KIMI_CODE_EXPERIMENTAL_FLAG=1` also enables it. The TUI currently ignores this field. It never names a concrete model alias, and resumed subagents keep their existing model. The selected preference is shown to the main agent alongside the profile description so it can still pass an explicit `model` when a task needs a different choice.
|
||||
`model_preference` applies only to newly spawned subagents when the secondary-model experiment is enabled — set `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL=1`, or the master `KIMI_CODE_EXPERIMENTAL_FLAG=1`. It takes effect in every launch mode, including the interactive TUI. The field never names a concrete model alias, and resumed subagents keep their existing model. The selected preference is shown to the main agent alongside the profile description so it can still pass an explicit `model` when a task needs a different choice.
|
||||
|
||||
A file with invalid content discovered in a directory is skipped with a warning and does not affect other files. A file passed explicitly via `--agent-file` must be valid — otherwise the CLI reports the error and exits.
|
||||
|
||||
|
|
@ -121,24 +123,27 @@ Custom agents delegated as sub-agents run without the built-in sub-agent framing
|
|||
|
||||
### Selecting the Main Agent
|
||||
|
||||
Two CLI flags select which agent drives the session. **Both are currently available only under `kimi -p` with `KIMI_CODE_EXPERIMENTAL_FLAG=1`**; the interactive TUI rejects them with a clear error for now:
|
||||
Two CLI flags select which agent drives a new session, in both print mode (`kimi -p`) and the interactive TUI:
|
||||
|
||||
- **`--agent <name>`**: Start the session with the named agent as the main Agent. The name can refer to a built-in agent or to any discovered file; an unknown name fails with an error listing the available agents.
|
||||
- **`--agent-file <path>`**: Load one agent file at the highest priority for this launch and start with it. The flag accepts exactly one file: it cannot be repeated, and it cannot be combined with `--agent`.
|
||||
|
||||
For example, in print mode:
|
||||
Both flags only apply when starting a new session — neither can be combined with `--session`/`--continue`. The agent is bound at session creation, and resuming restores the bound agent automatically, so no flag is needed (or allowed) on resume.
|
||||
|
||||
For example:
|
||||
|
||||
```sh
|
||||
KIMI_CODE_EXPERIMENTAL_FLAG=1 kimi -p --agent reviewer "Review the changes on this branch"
|
||||
kimi --agent reviewer
|
||||
kimi -p --agent reviewer "Review the changes on this branch"
|
||||
```
|
||||
|
||||
The bound agent is the session's identity: it is fixed at the session's first bind and cannot be switched later. Re-selecting the already-bound agent (for example resuming with the same `--agent`) is a no-op; selecting a different one fails with an "already bound" error.
|
||||
The bound agent is the session's identity: it is fixed at the session's first bind and cannot be switched later. In the TUI the flags bind only the startup session; a session created later in the same process (for example via `/new`) starts with the default agent.
|
||||
|
||||
For main-agent customization, reference `${base_prompt}` in the body so the environment, workspace-instruction, and Skill injections from the default prompt stay in effect; a body without `${base_prompt}` owns the entire prompt, which fits self-contained sub-agents.
|
||||
For main-agent customization, reference `${base_prompt}` in the body so the environment, workspace-instruction, Skill, and plugin injections already present in the effective default prompt stay in effect. When you want to replace the default prompt but keep only plugin-contributed instructions, use `${plugin_sections}` instead. A body without `${base_prompt}` or `${plugin_sections}` owns the entire prompt and excludes plugin instructions, which fits self-contained sub-agents.
|
||||
|
||||
### Overriding the main agent's system prompt with SYSTEM.md
|
||||
|
||||
To override the main agent's system prompt permanently — without passing `--agent` or `--agent-file` on every launch — write a `$KIMI_CODE_HOME/SYSTEM.md` file (default: `~/.kimi-code/SYSTEM.md`; it moves with `KIMI_CODE_HOME`). While the file exists and is non-empty, it replaces the built-in default main agent's system prompt in full — and only the prompt: the description and tool set are inherited from the built-in defaults. SYSTEM.md currently takes effect only under `kimi web` and under `kimi -p` with `KIMI_CODE_EXPERIMENTAL_FLAG=1`; the interactive TUI ignores the file.
|
||||
To override the main agent's system prompt permanently — without passing `--agent` or `--agent-file` on every launch — write a `$KIMI_CODE_HOME/SYSTEM.md` file (default: `~/.kimi-code/SYSTEM.md`; it moves with `KIMI_CODE_HOME`). While the file exists and is non-empty, it replaces the built-in default main agent's system prompt in full — and only the prompt: the description, tool set, and sub-agent delegation allowlist are inherited from the built-in defaults. SYSTEM.md takes effect in every launch mode, including interactive TUI sessions.
|
||||
|
||||
SYSTEM.md is a plain Markdown body — no frontmatter is required or read. A missing or empty file has no effect, and a read failure falls back to the built-in prompt with a warning. Explicit intent still outranks it: a project-scoped same-name agent file declaring `override: true` and any file passed via `--agent-file` take precedence, and selecting another agent with `--agent` bypasses it entirely. Within the user scope itself, SYSTEM.md wins over a same-name file discovered in the `agents/` directories.
|
||||
|
||||
|
|
@ -155,8 +160,9 @@ Like the body of a regular agent file, SYSTEM.md is rendered as a template each
|
|||
| `${now}` | Current time in ISO format |
|
||||
| `${additional_dirs_info}` | Additional directories added to the workspace; empty when there are none |
|
||||
| `${base_prompt}` | The default system prompt. Inside `SYSTEM.md` itself this is the built-in default; inside an agent file it is the effective default — the built-in default, or your `SYSTEM.md` override when present |
|
||||
| `${plugin_sections}` | A complete Plugin Instructions block contributed by enabled plugins; empty when no enabled plugin contributes instructions |
|
||||
|
||||
Unknown variables stay verbatim, a bare `$` is never special, and a variable with no context value renders as an empty string. Three pre-composed blocks — `${windows_notes}`, `${additional_dirs_section}`, and `${skills_section}` — render the matching built-in prompt section, or an empty string when it does not apply. The variables are enough to rebuild the skeleton of the built-in prompt, for example:
|
||||
Unknown variables stay verbatim, a bare `$` is never special, and a variable with no context value renders as an empty string. Four pre-composed blocks — `${windows_notes}`, `${additional_dirs_section}`, `${skills_section}`, and `${plugin_sections}` — render the matching built-in prompt section, or an empty string when it does not apply. The built-in default prompt already includes `${plugin_sections}`, so do not add it again when `${base_prompt}` already expands to that prompt. The variables are enough to rebuild the skeleton of the built-in prompt, for example:
|
||||
|
||||
These variables are evaluated when the system prompt is rendered. An existing session does not watch `AGENTS.md` or inject a change reminder automatically; edits take effect the next time the system prompt is rendered, such as after a working-directory change, context compaction, or a new session.
|
||||
|
||||
|
|
@ -166,6 +172,8 @@ You are Kimi, running at ${cwd} on ${os}.
|
|||
${agents_md}
|
||||
|
||||
${skills}
|
||||
|
||||
${plugin_sections}
|
||||
```
|
||||
|
||||
## Instruction Files
|
||||
|
|
|
|||
|
|
@ -153,5 +153,5 @@ This example only demonstrates the blocking mechanism — it is not a production
|
|||
|
||||
## Next steps
|
||||
|
||||
- [Configuration files](../configuration/config-files.md#hooks) — Full field reference for `[[hooks]]` in `config.toml`
|
||||
- [Configuration](#configuration) — Full field reference for `[[hooks]]` in `config.toml`
|
||||
- [Agents and sub-agents](./agents.md) — Use the `SubagentStop` event to trigger notifications after a sub-agent completes
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Plugins
|
||||
|
||||
Plugins package reusable Kimi Code CLI capabilities into installable units — they can add [Agent Skills](./skills.md), automatically load a specified Skill at session start, and declare MCP servers to provide real tool capabilities. They are ideal for sharing workflows with a team, connecting to external services, or installing extensions from the official marketplace.
|
||||
Plugins package reusable Kimi Code CLI capabilities into installable units — they can add [Agent Skills](./skills.md), custom [agents](./agents.md), automatically load a specified Skill at session start, contribute system-prompt instructions, and declare MCP servers to provide real tool capabilities. They are ideal for sharing workflows with a team, connecting to external services, or installing extensions from the official marketplace.
|
||||
|
||||
## Installation and Management
|
||||
|
||||
|
|
@ -143,6 +143,7 @@ Example:
|
|||
"version": "1.0.0",
|
||||
"description": "Finance data and analysis workflows for Kimi Code CLI",
|
||||
"skills": "./skills/",
|
||||
"systemPromptPath": "./SYSTEM.md",
|
||||
"sessionStart": {
|
||||
"skill": "using-finance"
|
||||
},
|
||||
|
|
@ -161,14 +162,36 @@ Supported fields:
|
|||
| `version`, `description`, `keywords`, `author`, `homepage`, `license` | Display metadata |
|
||||
| `interface` | Fields shown in `/plugins`: `displayName`, `shortDescription`, `longDescription`, `developerName`, `websiteURL` |
|
||||
| `skills` | One or more `./` paths; must be within the plugin root directory. When omitted, the `SKILL.md` in the root directory is treated as a single Skill root |
|
||||
| `agents` | One or more `./` paths; must be within the plugin root directory and point to directories containing [agent files](./agents.md#custom-agents). When omitted, the `agents/` directory under the plugin root (if present) is picked up automatically |
|
||||
| `sessionStart.skill` | Loads the specified plugin Skill into the main Agent when a new or resumed session starts |
|
||||
| `skillInstructions` | Additional instructions appended whenever a Skill from this plugin is loaded |
|
||||
| `systemPrompt` | Inline instructions contributed to the agent's system prompt while the plugin is enabled |
|
||||
| `systemPromptPath` | A `./` path to a UTF-8 text file containing system-prompt instructions; combined after `systemPrompt` when both are present |
|
||||
| `mcpServers` | MCP server declarations; enabled by default, can be disabled from `/plugins` |
|
||||
| `hooks` | Hook rules run on lifecycle events while the plugin is enabled; see [Hooks in Plugins](#hooks-in-plugins) |
|
||||
| `commands` | One or more `./` paths pointing to a directory or `.md` file; registers the Markdown files within as slash commands. See [Plugin Slash Commands](#plugin-slash-commands) |
|
||||
|
||||
Unsupported runtime fields such as `tools`, `apps`, `inject`, and `configFile` appear as diagnostics and are ignored.
|
||||
|
||||
### System-prompt instructions
|
||||
|
||||
Use `systemPrompt` for a short inline instruction, or `systemPromptPath` to keep longer instructions in a file inside the plugin root. If both fields are present, the inline text appears first, followed by the file content. The file content is read when the plugin is installed or reloaded, so edits take effect only after `/plugins reload`. For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "code-review",
|
||||
"systemPromptPath": "./SYSTEM.md"
|
||||
}
|
||||
```
|
||||
|
||||
System-prompt contributions take effect on both agent engines: the interactive TUI and `kimi -p` (the v1 engine), `kimi web`, and any CLI surface with `KIMI_CODE_EXPERIMENTAL_FLAG=1` (the v2 engine).
|
||||
|
||||
Each field — the inline `systemPrompt` and the `systemPromptPath` file — is limited to 32 KB (UTF-8 bytes): oversized content is ignored and reported in the plugin diagnostics. Across all enabled plugins, one prompt build injects at most 64 KB of instructions; contributions beyond the budget are skipped with a warning, including a single plugin whose inline text and file together exceed that budget.
|
||||
|
||||
New sessions and newly created agents read the contributions from the plugins currently enabled. An in-flight request keeps its existing system prompt. `/plugins reload` refreshes the plugin skill list and requests prompt rebuilds for live agents; use it when you need the change to converge deliberately before the next turn. On the v2 engine, installing, enabling, disabling, or removing a plugin updates the catalog immediately and a later prompt rebuild — for example after compaction or a tool-policy change — may pick up the new sections. The legacy engine keeps each live session's plugin snapshot until `/plugins reload` or a new session. A resumed session starts from its persisted prompt, and later rebuilds follow the engine-specific behavior above. Toggling a plugin's MCP server does not change system-prompt sections.
|
||||
|
||||
The built-in agent prompt includes instructions from enabled plugins automatically. A custom `SYSTEM.md` or agent file owns its template, so include `${plugin_sections}` where plugin-contributed instructions should appear. If the custom template includes `${base_prompt}` and that effective default already contains the plugin block, do not add `${plugin_sections}` again. See [Custom agents and SYSTEM.md](./agents.md#overriding-the-main-agent-s-system-prompt-with-system-md) for the complete variable table.
|
||||
|
||||
## Plugin Slash Commands
|
||||
|
||||
Slash commands save a prompt you use often as a `/command`, so you can trigger it by typing the command instead of retyping the whole thing.
|
||||
|
|
@ -249,6 +272,19 @@ my-plugin/
|
|||
|
||||
Regardless of how a Skill is loaded (`sessionStart.skill`, `/skill:<name>`, or automatic model invocation), `skillInstructions` appears alongside that plugin's Skill.
|
||||
|
||||
## Plugin Agents
|
||||
|
||||
A plugin can ship custom agents: declare one or more `./` directories in the manifest's `agents` field (or simply place an `agents/` directory under the plugin root). The agent files inside use the same format as [custom agents](./agents.md#custom-agents) and, while the plugin is enabled, are discovered automatically and can be delegated to as sub-agents by the main Agent.
|
||||
|
||||
```text
|
||||
my-plugin/
|
||||
kimi.plugin.json
|
||||
agents/
|
||||
reviewer.md
|
||||
```
|
||||
|
||||
Plugin agents rank below every other file source: on a name collision, user-level, extra, project-level, and `--agent-file` agents all win over the plugin-provided one, and replacing a built-in agent still requires an explicit `override: true` in the frontmatter. After installing, enabling, disabling, or removing a plugin, the agent list refreshes in a new session (or on `/reload`); on the v2 engine the live session also refreshes after `/plugins reload`.
|
||||
|
||||
## MCP Servers in Plugins
|
||||
|
||||
When a plugin needs real tool capabilities, it can declare `mcpServers` in its manifest, reusing the [MCP](./mcp.md) schema.
|
||||
|
|
@ -323,4 +359,3 @@ Plugins have a limited loading scope. The following operations do not occur duri
|
|||
- All paths must remain within the plugin root directory after symbolic link resolution
|
||||
- MCP servers of enabled plugins start after `/reload` or in new sessions and can be disabled at any time from `/plugins`
|
||||
- Broken manifests or unsafe paths appear in `/plugins info <id>` diagnostics and do not affect other sessions
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ When collapsed tool call results exist in the history, press `Ctrl-O` to toggle
|
|||
|
||||
## Approval Panel
|
||||
|
||||
When the Agent initiates a tool call that requires confirmation, the TUI displays an approval panel. For the full approval workflow, see [Interaction & Input](../guides/interaction.md#审批流程). The available keys inside the panel are:
|
||||
When the Agent initiates a tool call that requires confirmation, the TUI displays an approval panel. For the full approval workflow, see [Interaction & Input](../guides/interaction.md#approval-flow). The available keys inside the panel are:
|
||||
|
||||
| Shortcut | Function |
|
||||
| --- | --- |
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ All flags are optional — run `kimi` directly to enter an interactive session:
|
|||
| `--auto` | | Start with auto permission mode; tool approvals are handled automatically and the Agent will not ask the user questions |
|
||||
| `--plan` | | Start a new session in Plan mode — the AI will prioritize read-only tools for exploration and planning |
|
||||
| `--skills-dir <dir>` | | Load Skills from the specified directory, replacing the automatically discovered user and project directories. Can be repeated |
|
||||
| `--agent <name>` | | Start the session with the specified agent as the main Agent (experimental `kimi -p` only) |
|
||||
| `--agent-file <path>` | | Load a custom agent from a Markdown file for this launch and select it (experimental `kimi -p` only). Cannot be repeated or combined with `--agent` |
|
||||
| `--agent <name>` | | Start a new session with the specified agent as the main Agent. Cannot be combined with `--session`/`--continue` |
|
||||
| `--agent-file <path>` | | Load a custom agent from a Markdown file for the new session and select it. Cannot be repeated or combined with `--agent`, `--session`, or `--continue` |
|
||||
| `--add-dir <dir>` | | Add an extra workspace directory for this session. Relative paths resolve against the current working directory. Can be repeated |
|
||||
|
||||
`-r` / `--resume` is a hidden alias for `--session`; `--yes` and `--auto-approve` are hidden aliases for `--yolo` and are not shown in help output.
|
||||
|
|
@ -98,13 +98,14 @@ There are two ways to specify Skills directories, with different semantics:
|
|||
|
||||
### Custom Agents
|
||||
|
||||
`--agent` and `--agent-file` select which agent drives the session. Both are currently available only under `kimi -p` with `KIMI_CODE_EXPERIMENTAL_FLAG=1`; any other launch rejects them with a clear error:
|
||||
`--agent` and `--agent-file` select which agent drives a new session, in both print mode (`kimi -p`) and the interactive TUI:
|
||||
|
||||
```sh
|
||||
KIMI_CODE_EXPERIMENTAL_FLAG=1 kimi -p --agent reviewer "Review the changes on this branch"
|
||||
kimi --agent reviewer
|
||||
kimi -p --agent reviewer "Review the changes on this branch"
|
||||
```
|
||||
|
||||
`--agent-file` registers a single agent file at the highest priority for this launch only and selects it; the flag cannot be repeated, and `--agent` and `--agent-file` are mutually exclusive. The selection is fixed at the session's first bind: resuming with the same `--agent` is a no-op, and switching to a different one fails with an "already bound" error. See [Agents and Sub-Agents](../customization/agents.md#custom-agents) for the agent file format and discovery directories.
|
||||
`--agent-file` registers a single agent file at the highest priority for this launch only and selects it; the flag cannot be repeated, and `--agent` and `--agent-file` are mutually exclusive. Both flags only apply when starting a new session — neither can be combined with `--session`/`--continue`, because the agent is bound at session creation and resuming restores the bound agent automatically. The selection is fixed at the session's first bind and cannot be switched later; in the TUI the flags bind only the startup session, and a session created later in the same process (for example via `/new`) starts with the default agent. See [Agents and Sub-Agents](../customization/agents.md#custom-agents) for the agent file format and discovery directories.
|
||||
|
||||
## Non-Interactive Execution
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,9 @@ Some commands are only available in the idle state. Executing these commands whi
|
|||
| --- | --- | --- | --- |
|
||||
| `/login` | — | Select an account or platform and log in: Kimi Code uses OAuth device-code flow; Kimi Platform uses API key login | No |
|
||||
| `/logout` | — | Clear credentials for the currently selected account | No |
|
||||
| `/provider` | — | Open the interactive provider manager to view, add, and remove configured providers. See [Platforms & Models — `/provider` and provider management](../configuration/providers.md#provider-与供应商管理) | Yes |
|
||||
| `/provider` | — | Open the interactive provider manager to view, add, and remove configured providers. See [Platforms & Models — `/provider` and provider management](../configuration/providers.md#provider-—-interactive-provider-management) | Yes |
|
||||
| `/model` | — | Switch the LLM model used in the current session | Yes |
|
||||
| `/secondary_model` | — | Configure the secondary model used by subagents (writes the [`[secondary_model]`](../configuration/config-files.md#secondary-model) section and applies to the current session immediately). Requires the `secondary-model` experiment | Yes |
|
||||
| `/settings` | `/config` | Open the settings panel inside the TUI | Yes |
|
||||
| `/experiments` | `/experimental` | Open the experimental feature panel | Yes |
|
||||
| `/permission` | — | Select a permission mode | Yes |
|
||||
|
|
|
|||
|
|
@ -89,9 +89,9 @@ Collaboration tools handle inter-Agent coordination, user interaction, and Skill
|
|||
| `AskUserQuestion` | Auto-allow | Ask the user a question to gather structured input |
|
||||
| `Skill` | Auto-allow | Invoke a registered inline Skill |
|
||||
|
||||
**`Agent`** delegates a subtask to a sub-Agent. Required parameters: `prompt` (complete task description) and `description` (a 3–5 word short summary). Optional parameters: `subagent_type` (defaults to `coder`), `resume` (ID of an existing Agent to resume; mutually exclusive with `subagent_type`), `run_in_background` (defaults to false), and `model` (`"secondary"` for the secondary model configured via `[secondary_model] model`, or `"primary"` for the main model; ignored when resuming; available when the secondary-model experiment is enabled under `kimi web` or experimental `kimi -p`, not in the TUI). An explicit `model` overrides the selected [agent profile's `model_preference`](../customization/agents.md#agent-file-format); without either, the configured secondary model is the default, or the subagent inherits the caller's model when no secondary model is configured. Agent tasks time out after 2 hours by default; the limit is configurable via `[subagent] timeout_ms` in `config.toml` (`0` = no timeout, or the `KIMI_SUBAGENT_TIMEOUT_MS` env var), and defaults to no timeout in print mode (`kimi -p`). In foreground mode the parent Agent waits for the sub-Agent to complete before continuing; in background mode a task ID is returned immediately and the result is automatically delivered back to the main Agent via a synthetic User message when done. When several foreground `Agent` calls run in the same step, the TUI groups them and shows each subagent's running, waiting, completed, or failed status with elapsed time. See [Agent & Sub-Agents](../customization/agents.md) for details.
|
||||
**`Agent`** delegates a subtask to a sub-Agent. Required parameters: `prompt` (complete task description) and `description` (a 3–5 word short summary). Optional parameters: `subagent_type` (defaults to `coder`), `resume` (ID of an existing Agent to resume; mutually exclusive with `subagent_type`), `run_in_background` (defaults to false), and `model` (`"secondary"` for the secondary model configured via `[secondary_model] model`, or `"primary"` for the main model; ignored when resuming; available when the secondary-model experiment is enabled). An explicit `model` overrides the selected [agent profile's `model_preference`](../customization/agents.md#agent-file-format); without either, the configured secondary model is the default, or the subagent inherits the caller's model when no secondary model is configured. Agent tasks time out after 2 hours by default; the limit is configurable via `[subagent] timeout_ms` in `config.toml` (`0` = no timeout, or the `KIMI_SUBAGENT_TIMEOUT_MS` env var), and defaults to no timeout in print mode (`kimi -p`). In foreground mode the parent Agent waits for the sub-Agent to complete before continuing; in background mode a task ID is returned immediately and the result is automatically delivered back to the main Agent via a synthetic User message when done. When several foreground `Agent` calls run in the same step, the TUI groups them and shows each subagent's running, waiting, completed, or failed status with elapsed time. See [Agent & Sub-Agents](../customization/agents.md) for details.
|
||||
|
||||
**`AgentSwarm`** launches subagents from a shared `prompt_template` and an `items` array, resumes existing subagents through `resume_agent_ids`, or combines both in one call. The template must contain the `{{item}}` placeholder; each item replaces that placeholder and launches one new subagent. Pass `subagent_type` to choose the profile used by every spawned subagent in the swarm, or omit it to use `coder`. Pass `model` (available when the secondary-model experiment is enabled under `kimi web` or experimental `kimi -p`, not in the TUI) to run item-spawned subagents on the secondary model configured via `[secondary_model] model` (`"secondary"`) or the main model (`"primary"`). This explicit choice overrides the selected [agent profile's `model_preference`](../customization/agents.md#agent-file-format); without either, the configured secondary model is the default, or the subagent inherits the caller's model when no secondary model is configured. Resumed subagents keep their own model. Without `resume_agent_ids`, the tool requires at least 2 items; with `resume_agent_ids`, it can resume one or more existing subagents. The tool supports up to 128 total subagents, waits for all subagents to finish, and returns an aggregated report. In the TUI, foreground swarms show a live `Agent swarm` progress panel above the input box. If a model response calls `AgentSwarm`, that call must be the only tool call in the response; to run multiple swarms, call one `AgentSwarm`, wait for its result, then call the next, or combine the work into one swarm when a single template can cover it. In `manual` permission mode, `AgentSwarm` calls outside active swarm mode request approval unless a permission rule allows them; while swarm mode is active, `AgentSwarm` itself is auto-approved. Permission rules match `AgentSwarm` by tool name only — argument patterns such as `AgentSwarm(swarm)` are not supported. By default the tool ramps up concurrency without an upper limit (5 subagents start immediately, then 1 more every 700 ms); set `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` to a positive integer to cap how many subagents run at the same time during that ramp, or leave it unset for no cap. If it is set to a value that is not a positive integer, the AgentSwarm call fails fast.
|
||||
**`AgentSwarm`** launches subagents from a shared `prompt_template` and an `items` array, resumes existing subagents through `resume_agent_ids`, or combines both in one call. The template must contain the `{{item}}` placeholder; each item replaces that placeholder and launches one new subagent. Pass `subagent_type` to choose the profile used by every spawned subagent in the swarm, or omit it to use `coder`. Pass `model` (available when the secondary-model experiment is enabled) to run item-spawned subagents on the secondary model configured via `[secondary_model] model` (`"secondary"`) or the main model (`"primary"`). This explicit choice overrides the selected [agent profile's `model_preference`](../customization/agents.md#agent-file-format); without either, the configured secondary model is the default, or the subagent inherits the caller's model when no secondary model is configured. Resumed subagents keep their own model. Without `resume_agent_ids`, the tool requires at least 2 items; with `resume_agent_ids`, it can resume one or more existing subagents. The tool supports up to 128 total subagents, waits for all subagents to finish, and returns an aggregated report. In the TUI, foreground swarms show a live `Agent swarm` progress panel above the input box. If a model response calls `AgentSwarm`, that call must be the only tool call in the response; to run multiple swarms, call one `AgentSwarm`, wait for its result, then call the next, or combine the work into one swarm when a single template can cover it. In `manual` permission mode, `AgentSwarm` calls outside active swarm mode request approval unless a permission rule allows them; while swarm mode is active, `AgentSwarm` itself is auto-approved. Permission rules match `AgentSwarm` by tool name only — argument patterns such as `AgentSwarm(swarm)` are not supported. By default the tool ramps up concurrency without an upper limit (5 subagents start immediately, then 1 more every 700 ms); set `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` to a positive integer to cap how many subagents run at the same time during that ramp, or leave it unset for no cap. If it is set to a value that is not a positive integer, the AgentSwarm call fails fast.
|
||||
|
||||
**`AskUserQuestion`** asks the user a structured multiple-choice question — useful for disambiguation or option selection. The `questions` parameter accepts 1–4 questions; each question requires `question` (ending with `?`), `options` (2–4 choices, each with a `label` and `description`), and optional `header` (max 12 characters) and `multi_select` (defaults to false). An "Other" option is appended automatically. Setting `background` to true starts a background question task and returns a task ID immediately. When the host does not support interactive questioning, a failure message is returned and the Agent should ask the user directly in a text reply instead.
|
||||
|
||||
|
|
@ -109,13 +109,13 @@ Background task tools manage tasks started via `Bash`, `Agent`, or `AskUserQuest
|
|||
|
||||
**`TaskList`** returns the list of background tasks. Optional parameters: `active_only` (defaults to true; lists only running tasks) and `limit` (defaults to 20; range 1–100).
|
||||
|
||||
**`TaskOutput`** returns the status and output of a task given its `task_id`. The inline preview includes at most the most recent 32 KB of content; the full log is saved to disk, and the tool also returns an `output_path` with a suggestion to use `Read` for paginated access. Optional `block` (defaults to false) and `timeout` (seconds to wait; defaults to 30; range 0–3600) parameters allow waiting for the task to complete before returning.
|
||||
**`TaskOutput`** returns the status and output of a task given its `task_id`. The inline preview includes at most the most recent 32 KB of content; the full log is saved to disk, and the tool also returns an `output_path` with a suggestion to use `Read` for paginated access. The call is always non-blocking — it returns the current snapshot immediately, and task completion is delivered via automatic notification.
|
||||
|
||||
**`TaskStop`** accepts a `task_id` and optional `reason` (defaults to `Stopped by TaskStop`). Safe to call on tasks that are already in a terminal state.
|
||||
|
||||
## Scheduled Tasks
|
||||
|
||||
Scheduled task tools allow the Agent to re-inject a prompt into the current session at a future time — either as a one-time reminder or as a recurring cron-triggered task (periodic checks, daily reports, deployment monitoring, etc.). Schedules are bound to the session and remain active when you resume it with `kimi --session`, but are not carried into a brand-new session. A single session can hold at most 50 active scheduled tasks. Set `KIMI_DISABLE_CRON=1` to disable them entirely; see [Environment Variables](../configuration/env-vars.md#运行时开关).
|
||||
Scheduled task tools allow the Agent to re-inject a prompt into the current session at a future time — either as a one-time reminder or as a recurring cron-triggered task (periodic checks, daily reports, deployment monitoring, etc.). Schedules are bound to the session and remain active when you resume it with `kimi --session`, but are not carried into a brand-new session. A single session can hold at most 50 active scheduled tasks. Set `KIMI_DISABLE_CRON=1` to disable them entirely; see [Environment Variables](../configuration/env-vars.md#runtime-switches).
|
||||
|
||||
| Tool | Default Approval | Description |
|
||||
| --- | --- | --- |
|
||||
|
|
|
|||
|
|
@ -6,6 +6,39 @@ outline: 2
|
|||
|
||||
This page documents the changes in each Kimi Code CLI release.
|
||||
|
||||
## 0.31.0 (2026-07-30)
|
||||
|
||||
### Features
|
||||
|
||||
- Support Markdown-defined custom agents on agent-core.
|
||||
- Add the /secondary_model slash command to configure the secondary model used by subagents (experimental; enable it in /experiments first).
|
||||
- Plugins can contribute custom agents, discovered automatically and available for sub-agent delegation.
|
||||
- Plugins can contribute system prompt instructions through `systemPrompt` or `systemPromptPath` in `kimi.plugin.json`.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Remove the blocking `block`/`timeout` wait from the TaskOutput tool so checking a background task can no longer stall the conversation; it now always returns an immediate snapshot, and completion still arrives via automatic notification.
|
||||
- Fix sessions missing from the session picker when their cached metadata predates the archived flag.
|
||||
- Fix request headers not being passed correctly on some requests.
|
||||
|
||||
## 0.30.0 (2026-07-29)
|
||||
|
||||
### Features
|
||||
|
||||
- Add a customizable footer status line, configured via `[status_line]` in `tui.toml`.
|
||||
|
||||
### Polish
|
||||
|
||||
- Show a quota note after installing official plugins that bill against plan quota (such as Kimi Datasource).
|
||||
- Show a notice when an official plugin used in the session has an update available — run /plugins to update.
|
||||
- Remove the 50 MB size limit on file uploads to the built-in server.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fail fast when account quota or balance is exhausted instead of silently retrying for ~3 minutes.
|
||||
- Stop the turn after repeated invalid tool calls instead of retrying indefinitely.
|
||||
- web: Fix garbled line numbers in code blocks.
|
||||
|
||||
## 0.29.2 (2026-07-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ timeout = 5
|
|||
| `providers` | `table` | `{}` | API 供应商表 → [`providers`](#providers) |
|
||||
| `models` | `table` | — | 模型别名表 → [`models`](#models) |
|
||||
| `thinking` | `table` | — | Thinking 模式默认参数 → [`thinking`](#thinking) |
|
||||
| `loop_control` | `table` | — | Agent 循环控制参数 → [`loop_control`](#loop_control) |
|
||||
| `loop_control` | `table` | — | Agent 循环控制参数 → [`loop_control`](#loop-control) |
|
||||
| `background` | `table` | — | 后台任务运行参数 → [`background`](#background) |
|
||||
| `tools` | `table` | — | 全局工具开关 → [`tools`](#tools) |
|
||||
| `image` | `table` | — | 图片压缩参数 → [`image`](#image) |
|
||||
|
|
@ -192,7 +192,9 @@ display_name = "Kimi for Coding (custom)"
|
|||
|
||||
次主力模型是主模型 `default_model` 之外的第二个模型指针——通常是一个更便宜的模型,供不需要主模型的功能绑定使用。目前的消费者是子 Agent 派生:设置后,新派生的子 Agent(`Agent` / `AgentSwarm`)默认绑定该模型,而不再继承主 Agent 的模型;主 Agent 会被告知每次派生可在 `"secondary"`(该模型)与 `"primary"`(主模型)之间选择。未设置时,子 Agent 继承主 Agent 的模型。
|
||||
|
||||
该功能目前是实验功能,默认关闭。在 `kimi web` 下,通过 `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL=1` 启用;在 `kimi -p` 下,选择 v2 引擎本就需要 `KIMI_CODE_EXPERIMENTAL_FLAG=1`,该 master flag 也会启用本功能。交互式 TUI 会忽略该配置。
|
||||
该功能目前是实验功能,默认关闭。通过 `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL=1` 启用,或使用 master `KIMI_CODE_EXPERIMENTAL_FLAG=1`。它在包括交互式 TUI 在内的所有启动方式下生效。
|
||||
|
||||
在交互式 TUI 中,可以使用 [`/secondary_model`](../reference/slash-commands.md) 命令打开模型选择器来设置该配置:选择后会写入本小节配置,并在当前会话立即生效——之后派生的子 Agent 会直接绑定新的第二模型。
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
|
|
@ -384,7 +386,7 @@ MCP server 的声明配置写在 `~/.kimi-code/mcp.json` 或项目内 `.kimi-cod
|
|||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `theme` | `string` | `auto` | 配色主题:`auto`(跟随终端)、`dark`、`light`,或[自定义主题](../customization/themes)的名字 |
|
||||
| `theme` | `string` | `auto` | 配色主题:`auto`(跟随终端)、`dark`、`light`,或[自定义主题](../customization/themes.md)的名字 |
|
||||
| `disable_paste_burst` | `boolean` | `false` | 禁用非 bracketed paste 的粘贴突发兜底;默认开启,避免快速多行粘贴被逐行提交 |
|
||||
| `[editor].command` | `string` | `""` | 编写长输入用的外部编辑器命令;留空则回退到 `$VISUAL` / `$EDITOR` |
|
||||
| `[notifications].enabled` | `boolean` | `true` | 是否发送桌面通知 |
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export KIMI_DISABLE_TELEMETRY=1
|
|||
|
||||
### `KIMI_MODEL_*` 系列
|
||||
|
||||
不修改 `config.toml` 临时切换模型——设置 `KIMI_MODEL_NAME` 后,CLI 在内存里合成一个临时供应商,重启后失效。详见[用环境变量定义模型](#用环境变量定义模型kimi_model)。
|
||||
不修改 `config.toml` 临时切换模型——设置 `KIMI_MODEL_NAME` 后,CLI 在内存里合成一个临时供应商,重启后失效。详见[用环境变量定义模型](#用环境变量定义模型-kimi-model)。
|
||||
|
||||
## 供应商凭证键(写在 config.toml 里)
|
||||
|
||||
|
|
@ -128,9 +128,9 @@ kimi
|
|||
| `KIMI_CODE_PLUGIN_MARKETPLACE_URL` | 覆盖 `/plugins` 加载的 plugin marketplace JSON,适合 dev loopback server、测试 CDN 文件或替换 marketplace 目录 | `https://code.kimi.com/kimi-code/plugins/marketplace.json`;也接受 `http://`、`file://` URL 和本地路径 |
|
||||
| `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` | 限制 AgentSwarm 初始提升并发阶段可同时运行的子 Agent 数量;不设置表示不限制 | 正整数;非法值会立即失败 |
|
||||
| `KIMI_SUBAGENT_TIMEOUT_MS` | 单个子 Agent(`Agent` / `AgentSwarm`)可运行的最长时间(毫秒);优先级高于 `config.toml` 的 `[subagent] timeout_ms`(默认 `7200000`,即 2 小时) | 正整数;非法值回退到配置或默认值 |
|
||||
| `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL` | 在 `kimi web` 下启用实验性的次主力模型功能;`kimi -p` 仍需通过 `KIMI_CODE_EXPERIMENTAL_FLAG=1` 选择 v2 引擎,该 master flag 也会启用本功能 | 真值:`1`/`true`/`yes`/`on`;假值:`0`/`false`/`no`/`off` |
|
||||
| `KIMI_SECONDARY_MODEL` | 次主力模型;优先级高于 `config.toml` 的 `[secondary_model] model`。次主力模型实验功能启用后,新派生的子 Agent 默认绑定该模型,而不再继承主 Agent 的模型(TUI 不支持) | 已配置 `[models]` 中的模型 id,如 `kimi-code/kimi-k2.5`;空白值被忽略 |
|
||||
| `KIMI_SECONDARY_EFFORT` | 次主力模型的 thinking effort;优先级高于 `config.toml` 的 `[secondary_model] default_effort`,仅在次主力模型及其实验功能均启用时生效(TUI 不支持) | effort 取值,如 `low`;空白值被忽略 |
|
||||
| `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL` | 在包括交互式 TUI 在内的所有启动方式下启用实验性的次主力模型功能;master `KIMI_CODE_EXPERIMENTAL_FLAG=1` 也会启用本功能 | 真值:`1`/`true`/`yes`/`on`;假值:`0`/`false`/`no`/`off` |
|
||||
| `KIMI_SECONDARY_MODEL` | 次主力模型;优先级高于 `config.toml` 的 `[secondary_model] model`。次主力模型实验功能启用后,新派生的子 Agent 默认绑定该模型,而不再继承主 Agent 的模型 | 已配置 `[models]` 中的模型 id,如 `kimi-code/kimi-k2.5`;空白值被忽略 |
|
||||
| `KIMI_SECONDARY_EFFORT` | 次主力模型的 thinking effort;优先级高于 `config.toml` 的 `[secondary_model] default_effort`,仅在次主力模型及其实验功能均启用时生效 | effort 取值,如 `low`;空白值被忽略 |
|
||||
| `KIMI_MCP_STARTUP_TIMEOUT_MS` | 所有 MCP server 的全局默认连接超时(毫秒);优先级高于 `config.toml` 的 `[mcp] startup_timeout_ms`,但低于 `mcp.json` 中单个 server 的 `startupTimeoutMs`(默认 `30000`) | `1` 到 `2147483647` 的整数;非法值被忽略 |
|
||||
| `KIMI_MCP_TOOL_TIMEOUT_MS` | 所有 MCP server 的全局默认单次工具调用超时(毫秒);优先级高于 `config.toml` 的 `[mcp] tool_timeout_ms`,但低于 `mcp.json` 中单个 server 的 `toolTimeoutMs`(默认 `60000`) | `1` 到 `2147483647` 的整数;非法值被忽略 |
|
||||
| `KIMI_LOOP_MAX_STEPS_PER_TURN` | Agent 单轮最大步数;优先级高于 `config.toml` 的 `[loop_control] max_steps_per_turn`(不设或 `0` 表示无上限) | 非负整数;非法值被忽略 |
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ Kimi Code CLI 有三个地方可以影响运行参数:配置文件、命令行
|
|||
|
||||
> `[providers.<name>.env]` 子表只是配置文件里的一段 TOML,不会真正写入 shell 环境变量。仅当对应的直接字段(`api_key` / `base_url`)为空时,CLI 才会查这里。
|
||||
|
||||
完整的凭证键名列表见[环境变量:供应商凭证键](./env-vars.md#供应商凭证键写在-configtoml-里)。
|
||||
完整的凭证键名列表见[环境变量:供应商凭证键](./env-vars.md#供应商凭证键-写在-config-toml-里)。
|
||||
|
||||
## 命令行选项
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ Kimi Code CLI 内置三种子 Agent,开箱即用,分别面向不同任务形
|
|||
|
||||
### Agent 目录
|
||||
|
||||
Kimi Code CLI 按作用域发现 Agent 文件,作用域越具体,优先级越高:**显式(`--agent-file`)> 项目 > 额外 > 用户 > 内置**。两个文件定义了相同的 `name` 时,高优先级作用域胜出。每个目录都会递归扫描 `.md` 文件。
|
||||
Kimi Code CLI 按作用域发现 Agent 文件,作用域越具体,优先级越高:**显式(`--agent-file`)> 项目 > 额外 > 用户 > Plugin > 内置**。两个文件定义了相同的 `name` 时,高优先级作用域胜出。每个目录都会递归扫描 `.md` 文件。
|
||||
|
||||
**用户级**(对所有项目生效):
|
||||
- `$KIMI_CODE_HOME/agents/`(默认:`~/.kimi-code/agents/`)
|
||||
|
|
@ -63,6 +63,8 @@ Kimi 专属的用户 Agent 目录随 `KIMI_CODE_HOME` 移动,通用的 `~/.age
|
|||
extra_agent_dirs = ["~/team-agents", ".agents/team-agents"]
|
||||
```
|
||||
|
||||
**Plugin 级**:已启用 plugin 在其 manifest 的 `agents` 字段中声明的目录(省略时自动采用 plugin 根下的 `agents/` 目录),见[插件 Agent](./plugins.md#插件-agent)。Plugin Agent 优先级仅高于内置 Agent。
|
||||
|
||||
**内置 Agent** 随 CLI 分发,优先级最低。目录中发现的文件不会仅凭同名覆盖内置 Agent;如确需替换,必须在 Frontmatter 中声明 `override: true`。通过 `--agent-file` 加载的文件视为显式启动意图,可以覆盖同名内置 Agent,优先级高于所有目录作用域,且仅对本次启动生效。另外,`$KIMI_CODE_HOME/SYSTEM.md` 可永久覆盖默认主 Agent 的系统提示词(它不参与 Agent 文件发现),其优先级交互见下文 SYSTEM.md 小节。
|
||||
|
||||
::: warning 信任模型
|
||||
|
|
@ -105,11 +107,11 @@ disallowedTools:
|
|||
|
||||
内置工具与用户工具按名称精确匹配(区分大小写);以 `mcp__` 开头的条目按 glob 匹配 MCP 工具。有三种写法永远匹配不到任何工具,在 profile 生效时会给出警告:`mcp__` 模式之外使用通配符(`disallowedTools` 里单独的 `*` 什么也禁不掉);不是完整 `mcp__<服务器>__<工具>` 形式的 `mcp__` 字面量(`mcp__github` 匹配不到任何工具 —— 匹配整个服务器要用 `mcp__github__*`);以及任何已注册或内置工具都没有的名字(通常是笔误,如把 `Read` 写成 `read`)。
|
||||
|
||||
正文即 Agent 的系统提示词,每次构建提示词时都会作为模板渲染:`${var}` 占位符替换为实时上下文值——未知变量保持原样,单独的 `$` 没有特殊含义,上下文中缺失的变量渲染为空字符串。`${base_prompt}` 会在你放置它的位置嵌入有效默认系统提示词(内置默认,或存在时为你的 `SYSTEM.md` 覆盖),因此文件可以"包裹"默认行为而不是替换它。可用变量见下文 SYSTEM.md 变量表。
|
||||
正文即 Agent 的系统提示词,每次构建提示词时都会作为模板渲染:`${var}` 占位符替换为实时上下文值——未知变量保持原样,单独的 `$` 没有特殊含义,上下文中缺失的变量渲染为空字符串。`${base_prompt}` 会在你放置它的位置嵌入有效默认系统提示词(内置默认,或存在时为你的 `SYSTEM.md` 覆盖),因此文件可以"包裹"默认行为而不是替换它。如果文件会替换默认提示词、但仍要保留已启用 plugin 提供的指令,请把 `${plugin_sections}` 放在希望出现这些指令的位置。可用变量见下文 SYSTEM.md 变量表。
|
||||
|
||||
未知字段会被忽略,新版本写的文件在旧版本上仍可读取。其他 Agent 工具的字段(如 Claude Code 的 `model`、OpenCode 的 `mode`)同样会被忽略;加上 `tools` 的逗号分隔写法和 `name` 缺省回退到文件名,Claude Code 与 OpenCode 风格的 Agent 文件一般可直接加载 —— 只含 `description` 和正文的最小文件可跨工具通用。
|
||||
|
||||
`model_preference` 仅在次主力模型实验功能启用时对新启动的子 Agent 生效。在 `kimi web` 下,设置 `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL=1`;在实验性 `kimi -p` 下,必需的 `KIMI_CODE_EXPERIMENTAL_FLAG=1` 也会启用该功能。TUI 目前会忽略此字段。该字段不用于填写具体模型 alias,已恢复的子 Agent 也会保持原模型。主 Agent 会在 profile 描述中看到这项偏好,因此仍可在某项任务需要不同选择时显式传入 `model`。
|
||||
`model_preference` 仅在次主力模型实验功能启用时对新启动的子 Agent 生效——设置 `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL=1`,或 master `KIMI_CODE_EXPERIMENTAL_FLAG=1`。它在包括交互式 TUI 在内的所有启动方式下生效。该字段不用于填写具体模型 alias,已恢复的子 Agent 也会保持原模型。主 Agent 会在 profile 描述中看到这项偏好,因此仍可在某项任务需要不同选择时显式传入 `model`。
|
||||
|
||||
目录中发现的非法文件会被跳过并告警,不影响其他文件。通过 `--agent-file` 显式传入的文件必须合法 —— 否则 CLI 会报错并退出。
|
||||
|
||||
|
|
@ -121,24 +123,27 @@ disallowedTools:
|
|||
|
||||
### 选择主 Agent
|
||||
|
||||
两个 CLI flag 用于选择驱动会话的 Agent。**目前二者仅在 `KIMI_CODE_EXPERIMENTAL_FLAG=1` 时的 `kimi -p` 下可用**;交互式 TUI 会以明确错误拒绝它们:
|
||||
两个 CLI flag 用于选择驱动新会话的 Agent,在 print 模式(`kimi -p`)和交互式 TUI 中均可使用:
|
||||
|
||||
- **`--agent <name>`**:以指定 Agent 作为主 Agent 启动会话。名称可以指向内置 Agent 或任何已发现的文件;名称不存在时会报错,并列出可用的 Agent。
|
||||
- **`--agent-file <path>`**:以最高优先级加载一个 Agent 文件(仅本次启动)并以其启动。该 flag 只接受一个文件:不可重复传入,也不能与 `--agent` 同时使用。
|
||||
|
||||
例如在 print 模式下:
|
||||
两个 flag 都仅在新建会话时有效——都不能与 `--session`/`--continue` 组合。Agent 在会话创建时绑定,恢复会话时会自动还原已绑定的 Agent,因此恢复时不需要(也不允许)携带这些 flag。
|
||||
|
||||
例如:
|
||||
|
||||
```sh
|
||||
KIMI_CODE_EXPERIMENTAL_FLAG=1 kimi -p --agent reviewer "审查这个分支上的改动"
|
||||
kimi --agent reviewer
|
||||
kimi -p --agent reviewer "审查这个分支上的改动"
|
||||
```
|
||||
|
||||
绑定的 Agent 即会话的身份:在会话首次绑定后即固定,之后不可切换。重复选择已绑定的 Agent(例如以相同的 `--agent` 恢复会话)是 no-op;选择不同的 Agent 会报 "already bound" 错误。
|
||||
绑定的 Agent 即会话的身份:在会话首次绑定后即固定,之后不可切换。在 TUI 中,这些 flag 只绑定启动时的会话;之后在同一进程内新建的会话(例如通过 `/new`)使用默认 Agent。
|
||||
|
||||
定制主 Agent 时,在正文中引用 `${base_prompt}` 可保持默认提示词的环境、工作区指令和 Skill 注入生效;不引用 `${base_prompt}` 的正文则完全拥有自己的提示词,适合自包含的子 Agent。
|
||||
定制主 Agent 时,在正文中引用 `${base_prompt}` 可保持有效默认提示词中已有的环境、工作区指令、Skill 和 plugin 注入生效。如果要替换默认提示词、但只保留 plugin 提供的指令,请改用 `${plugin_sections}`。正文同时不引用 `${base_prompt}` 和 `${plugin_sections}` 时,会完全拥有自己的提示词并排除 plugin 指令,适合自包含的子 Agent。
|
||||
|
||||
### 用 SYSTEM.md 覆盖主 Agent 的系统提示词
|
||||
|
||||
希望永久覆盖主 Agent 的系统提示词、而不必每次启动都传入 `--agent` 或 `--agent-file` 时,可以写一份 `$KIMI_CODE_HOME/SYSTEM.md`(默认:`~/.kimi-code/SYSTEM.md`,随 `KIMI_CODE_HOME` 移动)。文件存在且非空期间,它整体替换内置默认主 Agent 的系统提示词——但只替换提示词,描述与工具集仍沿用内置默认值。SYSTEM.md 目前仅在 `kimi web`,以及 `KIMI_CODE_EXPERIMENTAL_FLAG=1` 时的 `kimi -p` 下生效;交互式 TUI 会忽略该文件。
|
||||
希望永久覆盖主 Agent 的系统提示词、而不必每次启动都传入 `--agent` 或 `--agent-file` 时,可以写一份 `$KIMI_CODE_HOME/SYSTEM.md`(默认:`~/.kimi-code/SYSTEM.md`,随 `KIMI_CODE_HOME` 移动)。文件存在且非空期间,它整体替换内置默认主 Agent 的系统提示词——但只替换提示词,描述、工具集与允许委派的子 Agent 列表仍沿用内置默认值。SYSTEM.md 在包括交互式 TUI 会话在内的所有启动方式下生效。
|
||||
|
||||
SYSTEM.md 是纯 Markdown 正文,不需要也不读取 Frontmatter。文件缺失或为空时不生效;读取失败时会告警并回退到内置提示词。优先级上,显式意图仍然胜出:项目作用域中声明了 `override: true` 的同名 Agent 文件、通过 `--agent-file` 传入的文件都排在 SYSTEM.md 之前,用 `--agent` 选择其他 Agent 时 SYSTEM.md 也不会生效;而在用户作用域内部,SYSTEM.md 优先于 `agents/` 目录中扫描到的同名文件。
|
||||
|
||||
|
|
@ -155,8 +160,9 @@ SYSTEM.md 是纯 Markdown 正文,不需要也不读取 Frontmatter。文件缺
|
|||
| `${now}` | 当前时间(ISO 格式) |
|
||||
| `${additional_dirs_info}` | 加入工作区的额外目录信息;没有时为空 |
|
||||
| `${base_prompt}` | 默认系统提示词。在 `SYSTEM.md` 中指内置默认提示词;在 Agent 文件中指有效默认提示词(内置默认,或存在时为你的 `SYSTEM.md` 覆盖) |
|
||||
| `${plugin_sections}` | 已启用 plugin 提供的完整 Plugin Instructions 块;没有已启用 plugin 提供指令时为空 |
|
||||
|
||||
未知变量原样保留,单独的 `$` 没有特殊含义;上下文中缺失的变量渲染为空字符串。另有三个预组合块——`${windows_notes}`、`${additional_dirs_section}`、`${skills_section}`——渲染对应的内置提示词段落,不适用时为空字符串。利用这些变量可以重建内置提示词的骨架,例如:
|
||||
未知变量原样保留,单独的 `$` 没有特殊含义;上下文中缺失的变量渲染为空字符串。另有四个预组合块——`${windows_notes}`、`${additional_dirs_section}`、`${skills_section}`、`${plugin_sections}`——渲染对应的内置提示词段落,不适用时为空字符串。内置默认提示词已经包含 `${plugin_sections}`;当 `${base_prompt}` 已展开为该提示词时,不要再重复加入此变量。利用这些变量可以重建内置提示词的骨架,例如:
|
||||
|
||||
这些变量会在系统提示词重新构建时求值。现有会话不会监控 `AGENTS.md`,也不会自动注入变更提醒;编辑文件后,需要等系统提示词再次构建(例如切换工作目录、上下文压缩或启动新会话)才会生效。
|
||||
|
||||
|
|
@ -166,6 +172,8 @@ You are Kimi, running at ${cwd} on ${os}.
|
|||
${agents_md}
|
||||
|
||||
${skills}
|
||||
|
||||
${plugin_sections}
|
||||
```
|
||||
|
||||
## 指令文件
|
||||
|
|
|
|||
|
|
@ -153,5 +153,5 @@ process.stdin.on('end', () => {
|
|||
|
||||
## 下一步
|
||||
|
||||
- [配置文件](../configuration/config-files.md#hooks) — `[[hooks]]` 在 `config.toml` 中的完整字段声明
|
||||
- [配置](#配置) — `[[hooks]]` 在 `config.toml` 中的完整字段声明
|
||||
- [Agent 与子 Agent](./agents.md) — 利用 `SubagentStop` 事件在子 Agent 完成后触发通知
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Plugins
|
||||
|
||||
Plugins 把可复用的 Kimi Code CLI 能力打包成可安装单元——可以添加 [Agent Skills](./skills.md)、在会话启动时自动加载指定 Skill,也可以声明 MCP servers 来提供真实工具能力。适合把工作流共享给团队、连接外部服务,或从官方 marketplace 安装扩展。
|
||||
Plugins 把可复用的 Kimi Code CLI 能力打包成可安装单元——可以添加 [Agent Skills](./skills.md)、自定义 [Agent](./agents.md)、在会话启动时自动加载指定 Skill、提供系统提示词指令,也可以声明 MCP servers 来提供真实工具能力。适合把工作流共享给团队、连接外部服务,或从官方 marketplace 安装扩展。
|
||||
|
||||
## 安装与管理
|
||||
|
||||
|
|
@ -143,6 +143,7 @@ Plugin 是一个带 manifest 的目录或 zip 文件。Manifest 可以放在以
|
|||
"version": "1.0.0",
|
||||
"description": "Finance data and analysis workflows for Kimi Code CLI",
|
||||
"skills": "./skills/",
|
||||
"systemPromptPath": "./SYSTEM.md",
|
||||
"sessionStart": {
|
||||
"skill": "using-finance"
|
||||
},
|
||||
|
|
@ -161,14 +162,36 @@ Plugin 是一个带 manifest 的目录或 zip 文件。Manifest 可以放在以
|
|||
| `version`、`description`、`keywords`、`author`、`homepage`、`license` | 展示元数据 |
|
||||
| `interface` | 在 `/plugins` 中展示的字段:`displayName`、`shortDescription`、`longDescription`、`developerName`、`websiteURL` |
|
||||
| `skills` | 一个或多个 `./` 路径,必须位于 plugin 根目录内。省略时根目录的 `SKILL.md` 被当作单个 Skill root |
|
||||
| `agents` | 一个或多个 `./` 路径,必须位于 plugin 根目录内,指向含有 [Agent 文件](./agents.md#自定义-agent)的目录。省略时根下的 `agents/` 目录(若存在)被自动采用 |
|
||||
| `sessionStart.skill` | 在新会话或恢复会话开始时,把指定 plugin Skill 加载到主 Agent |
|
||||
| `skillInstructions` | 每次加载此 plugin 的 Skill 时一并附带的额外说明 |
|
||||
| `systemPrompt` | plugin 启用期间提供给 Agent 系统提示词的内联指令 |
|
||||
| `systemPromptPath` | 指向 UTF-8 文本文件的 `./` 路径;同时设置 `systemPrompt` 时,文件内容拼接在内联指令之后 |
|
||||
| `mcpServers` | MCP server 声明,默认启用,可从 `/plugins` 中禁用 |
|
||||
| `hooks` | 在 plugin 启用期间于生命周期事件上运行的 hook 规则;见[插件中的 Hooks](#插件中的-hooks) |
|
||||
| `commands` | 一个或多个 `./` 路径,指向目录或 `.md` 文件,把其中的 Markdown 文件注册为斜杠命令;见[插件斜杠命令](#插件斜杠命令) |
|
||||
|
||||
`tools`、`apps`、`inject`、`configFile` 等不支持的运行时字段会显示为 diagnostics 并被忽略。
|
||||
|
||||
### 系统提示词指令
|
||||
|
||||
短指令可以直接写在 `systemPrompt`,较长内容则用 `systemPromptPath` 指向 plugin 根目录内的文件。两个字段同时存在时,内联文本在前,文件内容在后。文件内容在安装或重载 plugin 时读取,因此修改文件后需要 `/plugins reload` 才会生效。例如:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "code-review",
|
||||
"systemPromptPath": "./SYSTEM.md"
|
||||
}
|
||||
```
|
||||
|
||||
系统提示词贡献在两个 Agent 引擎上都生效:交互式 TUI 与 `kimi -p`(v1 引擎)、`kimi web`,以及 `KIMI_CODE_EXPERIMENTAL_FLAG=1` 时的所有 CLI 界面(v2 引擎)。
|
||||
|
||||
`systemPrompt` 字段与 `systemPromptPath` 文件各限制为 32 KB(UTF-8 字节):超限内容会被忽略,并显示在 plugin 的 diagnostics 中。一次提示词构建最多注入所有已启用 plugin 合计 64 KB 的指令;超出预算的贡献会被跳过并给出警告——单个 plugin 的内联文本与文件合计超过该预算时同样整体跳过。
|
||||
|
||||
新会话和新建 Agent 会读取当前已启用 plugin 的指令。正在进行的请求会继续使用已有的系统提示词。`/plugins reload` 会刷新 plugin Skill 列表,并请求重建活跃 Agent 的提示词;如果需要让变更在下一轮前明确收敛,请使用这个命令。在 v2 引擎中,安装、启用、禁用或移除 plugin 会立即更新 catalog,后续的提示词重建(例如压缩上下文或修改工具策略后)可能会读取新的指令。legacy 引擎会让每个活跃 session 保留自己的 plugin 快照,直到 `/plugins reload` 或创建新 session。从磁盘恢复的 session 会先使用持久化的提示词,后续重建再遵循对应引擎的行为。切换 plugin 的 MCP server 不会改变系统提示词指令。
|
||||
|
||||
内置 Agent 提示词会自动包含已启用 plugin 的指令。自定义 `SYSTEM.md` 或 Agent 文件完全拥有自己的模板,因此应在希望出现 plugin 指令的位置加入 `${plugin_sections}`。如果自定义模板包含 `${base_prompt}`,且该有效默认提示词已经包含 plugin 块,就不要再重复加入 `${plugin_sections}`。完整变量表见 [自定义 Agent 与 SYSTEM.md](./agents.md#用-system-md-覆盖主-agent-的系统提示词)。
|
||||
|
||||
## 插件斜杠命令
|
||||
|
||||
斜杠命令把一段常用提示词存成 `/命令`,输入它就能触发,省得每次重打。
|
||||
|
|
@ -249,6 +272,19 @@ my-plugin/
|
|||
|
||||
无论 Skill 通过哪种方式加载(`sessionStart.skill`、`/skill:<name>` 或模型自动调用),`skillInstructions` 都会随该 plugin 的 Skill 一起出现。
|
||||
|
||||
## 插件 Agent
|
||||
|
||||
Plugin 可以携带自定义 Agent:在 manifest 的 `agents` 字段里声明一个或多个 `./` 目录(或直接在 plugin 根下放置 `agents/` 目录),其中的 Agent 文件与[自定义 Agent](./agents.md#自定义-agent) 格式相同,会在 plugin 启用期间作为子 Agent 被主 Agent 自动发现和委派。
|
||||
|
||||
```text
|
||||
my-plugin/
|
||||
kimi.plugin.json
|
||||
agents/
|
||||
reviewer.md
|
||||
```
|
||||
|
||||
Plugin Agent 的优先级低于其他文件来源:同名时用户级、额外目录、项目级和 `--agent-file` 的 Agent 都会覆盖 plugin 提供的版本;替换内置 Agent 同样需要在 frontmatter 里显式写 `override: true`。安装、启用、禁用或移除 plugin 后,Agent 列表在新会话(或 `/reload`)时刷新;v2 引擎的当前会话还会在 `/plugins reload` 后刷新。
|
||||
|
||||
## Plugin 中的 MCP servers
|
||||
|
||||
当 plugin 需要真实工具能力时,可以在 manifest 中声明 `mcpServers`,复用 [MCP](./mcp.md) 的 schema。
|
||||
|
|
@ -323,4 +359,3 @@ Plugin 的加载范围有限,以下操作不会在安装或会话启动时发
|
|||
- 所有路径在解析符号链接后仍必须位于 plugin 根目录内
|
||||
- 已启用 plugin 的 MCP servers 会在 `/reload` 后或新会话中启动,且可随时从 `/plugins` 禁用
|
||||
- 损坏的 manifest 或不安全路径会显示在 `/plugins info <id>` 的 diagnostics 中,不影响其他会话
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ kimi <subcommand> [options]
|
|||
| `--auto` | | 以 auto 权限模式启动;工具审批自动处理,Agent 不会向用户提问 |
|
||||
| `--plan` | | 以 Plan 模式启动新会话,AI 会优先使用只读工具进行探索和规划 |
|
||||
| `--skills-dir <dir>` | | 从指定目录加载 Skills,替换自动发现的用户和项目目录。可重复传入 |
|
||||
| `--agent <name>` | | 以指定 Agent 作为主 Agent 启动会话(仅实验性 `kimi -p`) |
|
||||
| `--agent-file <path>` | | 从 Markdown 文件加载自定义 Agent(仅本次启动、仅实验性 `kimi -p`)并选中它。不可重复传入,也不能与 `--agent` 同时使用 |
|
||||
| `--agent <name>` | | 以指定 Agent 作为主 Agent 启动新会话。不能与 `--session`/`--continue` 同时使用 |
|
||||
| `--agent-file <path>` | | 从 Markdown 文件加载自定义 Agent 并为新会话选中它。不可重复传入,也不能与 `--agent`、`--session` 或 `--continue` 同时使用 |
|
||||
| `--add-dir <dir>` | | 为本次会话添加额外的工作目录。相对路径按当前工作目录解析。可重复传入 |
|
||||
|
||||
`-r` / `--resume` 是 `--session` 的隐藏别名;`--yes` 和 `--auto-approve` 是 `--yolo` 的隐藏别名,在帮助信息中不显示。
|
||||
|
|
@ -98,13 +98,14 @@ kimi --plan
|
|||
|
||||
### 自定义 Agent
|
||||
|
||||
`--agent` 和 `--agent-file` 用于选择驱动会话的 Agent。目前二者仅在 `KIMI_CODE_EXPERIMENTAL_FLAG=1` 时的 `kimi -p` 下可用,其他启动方式会以明确错误拒绝:
|
||||
`--agent` 和 `--agent-file` 用于选择驱动新会话的 Agent,在 print 模式(`kimi -p`)和交互式 TUI 中均可使用:
|
||||
|
||||
```sh
|
||||
KIMI_CODE_EXPERIMENTAL_FLAG=1 kimi -p --agent reviewer "审查这个分支上的改动"
|
||||
kimi --agent reviewer
|
||||
kimi -p --agent reviewer "审查这个分支上的改动"
|
||||
```
|
||||
|
||||
`--agent-file` 以最高优先级注册单个 Agent 文件(仅本次启动)并选中它;该 flag 不可重复传入,且 `--agent` 与 `--agent-file` 互斥。选择在会话首次绑定后即固定:以相同的 `--agent` 恢复会话是 no-op,换成不同的 Agent 会报 "already bound" 错误。Agent 文件格式与发现目录详见 [Agent 与子 Agent](../customization/agents.md#自定义-agent)。
|
||||
`--agent-file` 以最高优先级注册单个 Agent 文件(仅本次启动)并选中它;该 flag 不可重复传入,`--agent` 与 `--agent-file` 互斥。两个 flag 都仅在新建会话时有效——都不能与 `--session`/`--continue` 组合,因为 Agent 在会话创建时绑定,恢复会话时会自动还原已绑定的 Agent。选择在会话首次绑定后即固定,之后不可切换;在 TUI 中,这些 flag 只绑定启动时的会话,之后在同一进程内新建的会话(例如通过 `/new`)使用默认 Agent。Agent 文件格式与发现目录详见 [Agent 与子 Agent](../customization/agents.md#自定义-agent)。
|
||||
|
||||
## 非交互执行
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,9 @@
|
|||
| --- | --- | --- | --- |
|
||||
| `/login` | — | 选择账号或平台并登录:Kimi Code 走 OAuth 验证码流程,Kimi Platform 通过 API 密钥登录 | 否 |
|
||||
| `/logout` | — | 清除当前所选账号的凭据 | 否 |
|
||||
| `/provider` | — | 打开交互式供应商管理器,查看、添加和删除已配置的供应商。详见[平台与模型 — `/provider` 与供应商管理](../configuration/providers.md#provider-与供应商管理) | 是 |
|
||||
| `/provider` | — | 打开交互式供应商管理器,查看、添加和删除已配置的供应商。详见[平台与模型 — `/provider` 与供应商管理](../configuration/providers.md#provider-—-交互式供应商管理) | 是 |
|
||||
| `/model` | — | 切换当前会话使用的 LLM 模型 | 是 |
|
||||
| `/secondary_model` | — | 配置子 Agent 使用的第二模型(写入 [`[secondary_model]`](../configuration/config-files.md#secondary-model) 配置并在当前会话立即生效)。需开启 `secondary-model` 实验功能 | 是 |
|
||||
| `/settings` | `/config` | 打开 TUI 内的设置面板 | 是 |
|
||||
| `/experiments` | `/experimental` | 打开实验功能面板 | 是 |
|
||||
| `/permission` | — | 选择权限模式 | 是 |
|
||||
|
|
|
|||
|
|
@ -89,9 +89,9 @@ Plan 模式是一种受约束的工作状态:进入后 `Write` 与 `Edit` 只
|
|||
| `AskUserQuestion` | 自动放行 | 向用户提问以获取结构化输入 |
|
||||
| `Skill` | 自动放行 | 调用已注册的 inline Skill |
|
||||
|
||||
**`Agent`** 将子任务委托给子 Agent 执行。必填参数:`prompt`(完整任务描述)和 `description`(3–5 个词的简短说明)。可选参数:`subagent_type`(默认 `coder`)、`resume`(恢复已有 Agent 的 ID,与 `subagent_type` 互斥)、`run_in_background`(默认 false)和 `model`(`"secondary"` 表示 `[secondary_model] model` 配置的次主力模型,`"primary"` 表示主模型;resume 时无效;在 `kimi web` 或实验性 `kimi -p` 下启用次主力模型实验功能后可用,TUI 下被忽略)。显式 `model` 会覆盖所选 [Agent profile 的 `model_preference`](../customization/agents.md#agent-文件格式);两者均未设置时,已配置的次主力模型为默认值,未配置时则继承调用方模型。Agent 任务默认 2 小时超时,可通过 `config.toml` 的 `[subagent] timeout_ms`(`0` = 无超时,或 `KIMI_SUBAGENT_TIMEOUT_MS` 环境变量)配置,且在 print 模式(`kimi -p`)下默认无超时。前台模式下父 Agent 等待子 Agent 完成再继续;后台模式立即返回任务 ID,完成时通过合成 User 消息自动回到主 Agent。多个前台 `Agent` 调用在同一步运行时,TUI 会合并展示,并为每个子 Agent 显示运行、等待、完成或失败状态以及已耗时长。子 Agent 体系细节见 [Agent 与子 Agent](../customization/agents.md)。
|
||||
**`Agent`** 将子任务委托给子 Agent 执行。必填参数:`prompt`(完整任务描述)和 `description`(3–5 个词的简短说明)。可选参数:`subagent_type`(默认 `coder`)、`resume`(恢复已有 Agent 的 ID,与 `subagent_type` 互斥)、`run_in_background`(默认 false)和 `model`(`"secondary"` 表示 `[secondary_model] model` 配置的次主力模型,`"primary"` 表示主模型;resume 时无效;次主力模型实验功能启用后可用)。显式 `model` 会覆盖所选 [Agent profile 的 `model_preference`](../customization/agents.md#agent-文件格式);两者均未设置时,已配置的次主力模型为默认值,未配置时则继承调用方模型。Agent 任务默认 2 小时超时,可通过 `config.toml` 的 `[subagent] timeout_ms`(`0` = 无超时,或 `KIMI_SUBAGENT_TIMEOUT_MS` 环境变量)配置,且在 print 模式(`kimi -p`)下默认无超时。前台模式下父 Agent 等待子 Agent 完成再继续;后台模式立即返回任务 ID,完成时通过合成 User 消息自动回到主 Agent。多个前台 `Agent` 调用在同一步运行时,TUI 会合并展示,并为每个子 Agent 显示运行、等待、完成或失败状态以及已耗时长。子 Agent 体系细节见 [Agent 与子 Agent](../customization/agents.md)。
|
||||
|
||||
**`AgentSwarm`** 可以从共享的 `prompt_template` 和 `items` 数组启动子 Agent,也可以通过 `resume_agent_ids` 恢复已有子 Agent,或在一次调用中同时使用两者。模板必须包含 `{{item}}` 占位符;每个 item 会替换该占位符,并启动一个新的子 Agent。传入 `subagent_type` 可以指定整个 swarm 中所有新启动的子 Agent 使用的 profile;省略时默认使用 `coder`。传入 `model`(在 `kimi web` 或实验性 `kimi -p` 下启用次主力模型实验功能后可用,TUI 下被忽略)可以让新启动的子 Agent 运行在 `[secondary_model] model` 配置的次主力模型(`"secondary"`)或主模型(`"primary"`)上。这项显式选择会覆盖所选 [Agent profile 的 `model_preference`](../customization/agents.md#agent-文件格式);两者均未设置时,已配置的次主力模型为默认值,未配置时则继承调用方模型。恢复的子 Agent 保持其原有模型。不传 `resume_agent_ids` 时,本工具要求至少 2 个 item;传入 `resume_agent_ids` 时,可以恢复 1 个或多个已有子 Agent。本工具最多支持 128 个子 Agent,会等待全部子 Agent 完成,并返回聚合报告。在 TUI 中,前台 swarm 会在输入框上方显示实时 `Agent swarm` 进度面板。若一次模型响应调用 `AgentSwarm`,该调用必须是该响应中的唯一工具调用;如需运行多个 swarm,应先调用一个 `AgentSwarm` 并等待结果,再调用下一个,若单个模板可以覆盖这些工作,也可以合并为一个 swarm。在 `manual` 权限模式下,未处于 swarm mode 时调用 `AgentSwarm` 会触发审批,除非已有权限规则允许;swarm mode 已开启时,`AgentSwarm` 本身会自动放行。权限规则只能按工具名 `AgentSwarm` 匹配,不支持 `AgentSwarm(swarm)` 这类参数模式。默认情况下,本工具会逐步提升并发且不设上限(立即启动 5 个子 Agent,之后每 700 毫秒再启动 1 个);将 `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` 设为正整数可限制该阶段同时运行的子 Agent 数量,不设置则表示不限制。若设置为非正整数的值,本次 AgentSwarm 调用会立即失败。
|
||||
**`AgentSwarm`** 可以从共享的 `prompt_template` 和 `items` 数组启动子 Agent,也可以通过 `resume_agent_ids` 恢复已有子 Agent,或在一次调用中同时使用两者。模板必须包含 `{{item}}` 占位符;每个 item 会替换该占位符,并启动一个新的子 Agent。传入 `subagent_type` 可以指定整个 swarm 中所有新启动的子 Agent 使用的 profile;省略时默认使用 `coder`。传入 `model`(次主力模型实验功能启用后可用)可以让新启动的子 Agent 运行在 `[secondary_model] model` 配置的次主力模型(`"secondary"`)或主模型(`"primary"`)上。这项显式选择会覆盖所选 [Agent profile 的 `model_preference`](../customization/agents.md#agent-文件格式);两者均未设置时,已配置的次主力模型为默认值,未配置时则继承调用方模型。恢复的子 Agent 保持其原有模型。不传 `resume_agent_ids` 时,本工具要求至少 2 个 item;传入 `resume_agent_ids` 时,可以恢复 1 个或多个已有子 Agent。本工具最多支持 128 个子 Agent,会等待全部子 Agent 完成,并返回聚合报告。在 TUI 中,前台 swarm 会在输入框上方显示实时 `Agent swarm` 进度面板。若一次模型响应调用 `AgentSwarm`,该调用必须是该响应中的唯一工具调用;如需运行多个 swarm,应先调用一个 `AgentSwarm` 并等待结果,再调用下一个,若单个模板可以覆盖这些工作,也可以合并为一个 swarm。在 `manual` 权限模式下,未处于 swarm mode 时调用 `AgentSwarm` 会触发审批,除非已有权限规则允许;swarm mode 已开启时,`AgentSwarm` 本身会自动放行。权限规则只能按工具名 `AgentSwarm` 匹配,不支持 `AgentSwarm(swarm)` 这类参数模式。默认情况下,本工具会逐步提升并发且不设上限(立即启动 5 个子 Agent,之后每 700 毫秒再启动 1 个);将 `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` 设为正整数可限制该阶段同时运行的子 Agent 数量,不设置则表示不限制。若设置为非正整数的值,本次 AgentSwarm 调用会立即失败。
|
||||
|
||||
**`AskUserQuestion`** 以结构化多选题的形式向用户提问,适用于需要消歧或选择方案的场景。`questions` 参数接受 1–4 道题,每道题需提供 `question`(以 `?` 结尾)、`options`(2–4 个选项,每项含 `label` 和 `description`)以及可选的 `header`(最多 12 字符)和 `multi_select`(默认 false)。系统自动附加"其他"选项。`background` 为 true 时启动后台问题任务并立即返回任务 ID。宿主未实现交互式提问能力时返回失败提示,Agent 应改为在文本回复中直接提问。
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ Plan 模式是一种受约束的工作状态:进入后 `Write` 与 `Edit` 只
|
|||
|
||||
**`TaskList`** 返回后台任务列表。可选参数 `active_only`(默认 true,仅列出运行中的任务)和 `limit`(默认 20,取值范围 1–100)。
|
||||
|
||||
**`TaskOutput`** 根据 `task_id` 返回任务状态与输出。内联预览最多包含最近 32 KB 的内容;完整日志保存在磁盘上,工具会一并返回 `output_path` 并提示通过 `Read` 分页读取。可选 `block`(默认 false)和 `timeout`(等待秒数,默认 30,取值范围 0–3600)参数可用于等待任务完成后再返回。
|
||||
**`TaskOutput`** 根据 `task_id` 返回任务状态与输出。内联预览最多包含最近 32 KB 的内容;完整日志保存在磁盘上,工具会一并返回 `output_path` 并提示通过 `Read` 分页读取。该调用始终是非阻塞的——立即返回当前快照,任务完成会通过自动通知送达。
|
||||
|
||||
**`TaskStop`** 接受 `task_id` 和可选的 `reason`(默认 `Stopped by TaskStop`)。对已处于终止状态的任务也能安全调用。
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,39 @@ outline: 2
|
|||
|
||||
本页记录 Kimi Code CLI 每个版本的变更内容。
|
||||
|
||||
## 0.31.0(2026-07-30)
|
||||
|
||||
### 新功能
|
||||
|
||||
- TUI 支持 Markdown 定义的自定义 Agent。
|
||||
- 新增 /secondary_model 斜杠命令,用于配置子 Agent 使用的辅助模型(实验性功能,需先在 /experiments 中开启)。
|
||||
- 插件可贡献自定义 Agent,自动发现并可用于子 Agent 委派。
|
||||
- 插件可贡献系统提示词,通过 `kimi.plugin.json` 中的 `systemPrompt` 或 `systemPromptPath` 声明。
|
||||
|
||||
### 修复
|
||||
|
||||
- 移除 TaskOutput 工具的阻塞式 `block`/`timeout` 等待。
|
||||
- 修复会话元数据缓存早于 archived 标记时会话选择器缺少会话的问题。
|
||||
- 修复部分请求未能正确传递请求头的问题。
|
||||
|
||||
## 0.30.0(2026-07-29)
|
||||
|
||||
### 新功能
|
||||
|
||||
- 新增可自定义的底部状态栏,可通过 `tui.toml` 中的 `[status_line]` 配置。
|
||||
|
||||
### 优化
|
||||
|
||||
- 安装会计入套餐额度的官方插件(如 Kimi Datasource)后,显示额度说明。
|
||||
- 会话中使用的官方插件有可用更新时显示提示,可运行 /plugins 更新。
|
||||
- 移除内置服务器文件上传的 50 MB 大小限制。
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复账户额度或余额耗尽时静默重试约 3 分钟的问题,现在会立即报错。
|
||||
- 修复工具调用反复无效时无限重试的问题,现在会终止当前回合。
|
||||
- web: 修复代码块中行号乱码的问题。
|
||||
|
||||
## 0.29.2(2026-07-27)
|
||||
|
||||
### 修复
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
# @moonshot-ai/acp-adapter
|
||||
|
||||
## 0.3.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`40172c7`](https://github.com/MoonshotAI/kimi-code/commit/40172c7ca96ca981b043b793588dd32e898979fa), [`40172c7`](https://github.com/MoonshotAI/kimi-code/commit/40172c7ca96ca981b043b793588dd32e898979fa)]:
|
||||
- @moonshot-ai/agent-core@0.15.7
|
||||
- @moonshot-ai/kimi-code-sdk@0.15.0
|
||||
|
||||
## 0.3.5
|
||||
|
||||
### Patch Changes
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@moonshot-ai/acp-adapter",
|
||||
"version": "0.3.5",
|
||||
"version": "0.3.6",
|
||||
"private": true,
|
||||
"description": "Agent Client Protocol adapter for kimi-code",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,16 @@
|
|||
# @moonshot-ai/agent-core-v2
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#2382](https://github.com/MoonshotAI/kimi-code/pull/2382) [`40172c7`](https://github.com/MoonshotAI/kimi-code/commit/40172c7ca96ca981b043b793588dd32e898979fa) Thanks [@liruifengv](https://github.com/liruifengv)! - Replace the bootstrap `clientVersion` with a required `clientIdentity` host identity object: the OAuth device-flow endpoints now send the full `X-Msh-*` device headers on every host, telemetry reads the client version from the same source, and the session export manifest gains an optional desktop version field.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`40172c7`](https://github.com/MoonshotAI/kimi-code/commit/40172c7ca96ca981b043b793588dd32e898979fa)]:
|
||||
- @moonshot-ai/kimi-code-oauth@0.3.0
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
|
|
|||
15
packages/agent-core-v2/docs/state-manifest.d.ts
vendored
15
packages/agent-core-v2/docs/state-manifest.d.ts
vendored
|
|
@ -23,7 +23,7 @@
|
|||
// references become '(circular)', and class instances collapse to a '(ClassName)'
|
||||
// marker — the wire shape of an entry is the JSON projection of the type here.
|
||||
//
|
||||
// Index (Session: 28 keys · Agent: 69 keys)
|
||||
// Index (Session: 28 keys · Agent: 68 keys)
|
||||
// Session
|
||||
// cron.inFlight src/session/cron/sessionCronServiceImpl.ts
|
||||
// cron.lastSeenAt src/session/cron/sessionCronServiceImpl.ts
|
||||
|
|
@ -64,8 +64,6 @@
|
|||
// contextSize.lastEmittedTokens src/agent/contextSize/contextSizeService.ts
|
||||
// dateChange.seed src/agent/dateChange/dateChangeService.ts
|
||||
// externalHooks.stopHookContinuationUsed src/agent/externalHooks/externalHooksService.ts
|
||||
// faultInjection.armed src/agent/faultInjection/faultInjectionService.ts
|
||||
// faultInjection.fired src/agent/faultInjection/faultInjectionService.ts
|
||||
// fullCompaction.activeTurnId src/agent/fullCompaction/fullCompactionService.ts
|
||||
// fullCompaction.compactionCountInTurn src/agent/fullCompaction/fullCompactionService.ts
|
||||
// fullCompaction.consecutiveOverflowCompactions src/agent/fullCompaction/fullCompactionService.ts
|
||||
|
|
@ -99,6 +97,7 @@
|
|||
// plan.wasActive src/agent/plan/injection/planModeInjection.ts
|
||||
// profile.activeToolNamesOverlay src/agent/profile/profileService.ts
|
||||
// profile.agentsMdWarning src/agent/profile/profileService.ts
|
||||
// profile.emittedPluginBudgetWarnings src/agent/profile/profileService.ts
|
||||
// profile.emittedThinkingEffortWarnings src/agent/profile/profileService.ts
|
||||
// profile.emittedToolPatternWarnings src/agent/profile/profileService.ts
|
||||
// prompt.launching src/agent/prompt/promptService.ts
|
||||
|
|
@ -201,6 +200,7 @@ export interface SessionStateSnapshot {
|
|||
readonly now?: string;
|
||||
readonly skills?: string;
|
||||
readonly skillActive?: boolean;
|
||||
readonly pluginSections?: string;
|
||||
readonly productName?: string;
|
||||
readonly replyStyleGuide?: string;
|
||||
[key: string]: unknown;
|
||||
|
|
@ -216,6 +216,7 @@ export interface SessionStateSnapshot {
|
|||
readonly now?: string;
|
||||
readonly skills?: string;
|
||||
readonly skillActive?: boolean;
|
||||
readonly pluginSections?: string;
|
||||
readonly productName?: string;
|
||||
readonly replyStyleGuide?: string;
|
||||
[key: string]: unknown;
|
||||
|
|
@ -296,6 +297,7 @@ export interface SessionStateSnapshot {
|
|||
readonly now?: string;
|
||||
readonly skills?: string;
|
||||
readonly skillActive?: boolean;
|
||||
readonly pluginSections?: string;
|
||||
readonly productName?: string;
|
||||
readonly replyStyleGuide?: string;
|
||||
[key: string]: unknown;
|
||||
|
|
@ -311,6 +313,7 @@ export interface SessionStateSnapshot {
|
|||
readonly now?: string;
|
||||
readonly skills?: string;
|
||||
readonly skillActive?: boolean;
|
||||
readonly pluginSections?: string;
|
||||
readonly productName?: string;
|
||||
readonly replyStyleGuide?: string;
|
||||
[key: string]: unknown;
|
||||
|
|
@ -1089,9 +1092,6 @@ export interface AgentStateSnapshot {
|
|||
} | undefined;
|
||||
// src/agent/externalHooks/externalHooksService.ts
|
||||
'externalHooks.stopHookContinuationUsed': boolean;
|
||||
// src/agent/faultInjection/faultInjectionService.ts
|
||||
'faultInjection.armed': 'request-too-large' | 'image-format' | undefined;
|
||||
'faultInjection.fired': (/* FaultKind — packages/agent-core-v2/src/agent/faultInjection/faultInjection.ts */ 'request-too-large' | 'image-format')[];
|
||||
// src/agent/fullCompaction/fullCompactionService.ts
|
||||
'fullCompaction.activeTurnId': number | undefined;
|
||||
'fullCompaction.compactionCountInTurn': number;
|
||||
|
|
@ -1119,7 +1119,7 @@ export interface AgentStateSnapshot {
|
|||
'llmRequester.lastConfigLogSignature': string | undefined;
|
||||
'llmRequester.mediaDegradedTurns': Set<number>;
|
||||
'llmRequester.mediaStrippedTurns': Map<number, /* MediaStripSnapshot — packages/agent-core-v2/src/agent/contextProjector/contextProjector.ts */ {
|
||||
readonly "__@mediaStripSnapshotBrand@2736": undefined;
|
||||
readonly "__@mediaStripSnapshotBrand@2722": undefined;
|
||||
}>;
|
||||
'llmRequester.turnConfigs': Map<number, /* TurnRequestConfig — packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts */ {
|
||||
readonly resolved: /* ProfileModelContext — packages/agent-core-v2/src/agent/profile/profile.ts */ {
|
||||
|
|
@ -1198,6 +1198,7 @@ export interface AgentStateSnapshot {
|
|||
// src/agent/profile/profileService.ts
|
||||
'profile.activeToolNamesOverlay': readonly string[] | undefined;
|
||||
'profile.agentsMdWarning': string | undefined;
|
||||
'profile.emittedPluginBudgetWarnings': Set<string>;
|
||||
'profile.emittedThinkingEffortWarnings': Set<string>;
|
||||
'profile.emittedToolPatternWarnings': Set<string>;
|
||||
// src/agent/prompt/promptService.ts
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@moonshot-ai/agent-core-v2",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"description": "The unified agent engine for Kimi (v2 — DI Scope architecture)",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -206,7 +206,6 @@ const DOMAIN_LAYER = new Map([
|
|||
// the domain to L4 beside the other agent-behaviour tools.
|
||||
['edit', 4],
|
||||
['llmRequester', 4],
|
||||
['faultInjection', 4],
|
||||
['profile', 4],
|
||||
['prompt', 4],
|
||||
// `shellCommand` orchestrates user `!` commands through `toolRegistry` (L3),
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
/**
|
||||
* `faultInjection` domain (L4) — deterministic provider-failure simulation
|
||||
* for testing the requester's recovery projections over a live channel.
|
||||
*
|
||||
* The turn-loop recovery resends (media-degraded after an HTTP 413 body-size
|
||||
* rejection, media-stripped after an image-format rejection) are
|
||||
* deterministic given a provider error, but a real provider cannot be asked
|
||||
* to produce one on demand. Arming a one-shot fault makes the next LLM
|
||||
* request attempt raise the chosen error BEFORE the provider is contacted,
|
||||
* so the recovery path — projection rebuild, per-turn stickiness, wire
|
||||
* records — runs end-to-end while the (successful) resend still goes to the
|
||||
* real provider.
|
||||
*
|
||||
* `arm` is refused unless the `fault-injection` experimental flag is enabled
|
||||
* (see ./flag); `take` is the requester's consumption point and stays inert
|
||||
* otherwise.
|
||||
*/
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
||||
export type FaultKind = 'request-too-large' | 'image-format';
|
||||
|
||||
export interface FaultInjectionStatus {
|
||||
readonly armed: FaultKind | undefined;
|
||||
readonly fired: readonly FaultKind[];
|
||||
}
|
||||
|
||||
export interface IFaultInjectionService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
arm(kind: FaultKind): void;
|
||||
|
||||
status(): FaultInjectionStatus;
|
||||
|
||||
clear(): void;
|
||||
|
||||
take(): FaultKind | undefined;
|
||||
}
|
||||
|
||||
export const IFaultInjectionService: ServiceIdentifier<IFaultInjectionService> =
|
||||
createDecorator<IFaultInjectionService>('faultInjectionService');
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
/**
|
||||
* `faultInjection` domain (L4) — `IFaultInjectionService` implementation.
|
||||
*
|
||||
* Agent-scope one-shot latch: `arm` (flag-gated) stores the next fault,
|
||||
* `take` (the llmRequester's per-attempt consumption point) consumes and
|
||||
* records it. Both state slots (`armed`, `fired`) are registered into
|
||||
* `agentState` (`IAgentStateService`) and read/written through it. Bound at
|
||||
* Agent scope.
|
||||
*/
|
||||
|
||||
import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope';
|
||||
import { defineState } from '#/_base/state/stateRegistry';
|
||||
import { IAgentStateService } from '#/agent/state/agentState';
|
||||
import { IFlagService } from '#/app/flag/flag';
|
||||
import { ErrorCodes, Error2 } from '#/errors';
|
||||
|
||||
import { FAULT_INJECTION_FLAG_ID } from './flag';
|
||||
import {
|
||||
IFaultInjectionService,
|
||||
type FaultInjectionStatus,
|
||||
type FaultKind,
|
||||
} from './faultInjection';
|
||||
|
||||
export const faultInjectionArmedKey = defineState<FaultKind | undefined>(
|
||||
'faultInjection.armed',
|
||||
() => undefined as FaultKind | undefined,
|
||||
);
|
||||
export const faultInjectionFiredKey = defineState<FaultKind[]>('faultInjection.fired', () => []);
|
||||
|
||||
export class FaultInjectionService implements IFaultInjectionService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IFlagService private readonly flags: IFlagService,
|
||||
@IAgentStateService private readonly states: IAgentStateService,
|
||||
) {
|
||||
this.states.register(faultInjectionArmedKey);
|
||||
this.states.register(faultInjectionFiredKey);
|
||||
}
|
||||
|
||||
private get armed(): FaultKind | undefined {
|
||||
return this.states.get(faultInjectionArmedKey);
|
||||
}
|
||||
|
||||
private set armed(value: FaultKind | undefined) {
|
||||
this.states.set(faultInjectionArmedKey, value);
|
||||
}
|
||||
|
||||
private get fired(): FaultKind[] {
|
||||
return this.states.get(faultInjectionFiredKey);
|
||||
}
|
||||
|
||||
arm(kind: FaultKind): void {
|
||||
if (!this.flags.enabled(FAULT_INJECTION_FLAG_ID)) {
|
||||
throw new Error2(
|
||||
ErrorCodes.REQUEST_INVALID,
|
||||
'Fault injection is disabled; enable the fault-injection experimental flag ' +
|
||||
'(KIMI_CODE_EXPERIMENTAL_FAULT_INJECTION=1, the master flag, or the ' +
|
||||
'[experimental] config section).',
|
||||
);
|
||||
}
|
||||
this.armed = kind;
|
||||
}
|
||||
|
||||
status(): FaultInjectionStatus {
|
||||
return { armed: this.armed, fired: [...this.fired] };
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.armed = undefined;
|
||||
this.fired.length = 0;
|
||||
}
|
||||
|
||||
take(): FaultKind | undefined {
|
||||
const kind = this.armed;
|
||||
if (kind === undefined) return undefined;
|
||||
this.armed = undefined;
|
||||
this.fired.push(kind);
|
||||
return kind;
|
||||
}
|
||||
}
|
||||
|
||||
registerScopedService(
|
||||
LifecycleScope.Agent,
|
||||
IFaultInjectionService,
|
||||
FaultInjectionService,
|
||||
ScopeActivation.OnScopeCreated,
|
||||
'faultInjection',
|
||||
);
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
/**
|
||||
* `faultInjection` domain (L4) — registers the `fault-injection` experimental
|
||||
* flag into `flag`.
|
||||
*
|
||||
* Gates the fault-injection Service's `arm`: deterministic provider-failure
|
||||
* simulation for exercising the requester's recovery projections over a live
|
||||
* channel. Off by default; enable via
|
||||
* `KIMI_CODE_EXPERIMENTAL_FAULT_INJECTION`, the master
|
||||
* `KIMI_CODE_EXPERIMENTAL_FLAG`, or the `[experimental]` config section.
|
||||
* Imported for its side effect (registers the definition) from the package
|
||||
* barrel.
|
||||
*/
|
||||
|
||||
import { type FlagDefinitionInput, registerFlagDefinition } from '#/app/flag/flagRegistry';
|
||||
|
||||
export const FAULT_INJECTION_FLAG_ID = 'fault-injection';
|
||||
export const FAULT_INJECTION_FLAG_ENV = 'KIMI_CODE_EXPERIMENTAL_FAULT_INJECTION';
|
||||
|
||||
export const faultInjectionFlag: FlagDefinitionInput = {
|
||||
id: FAULT_INJECTION_FLAG_ID,
|
||||
title: 'Fault injection (LLM request failures)',
|
||||
description:
|
||||
'Allow arming a one-shot deterministic provider failure (HTTP 413 body-size or image-format rejection) on the next LLM request, for testing the media-degraded / media-stripped recovery projections over a live channel.',
|
||||
env: FAULT_INJECTION_FLAG_ENV,
|
||||
default: false,
|
||||
surface: 'core',
|
||||
};
|
||||
|
||||
registerFlagDefinition(faultInjectionFlag);
|
||||
|
|
@ -39,10 +39,6 @@ import {
|
|||
type MediaStripSnapshot,
|
||||
} from '#/agent/contextProjector/contextProjector';
|
||||
import { IAgentContextSizeService } from '#/agent/contextSize/contextSize';
|
||||
import {
|
||||
IFaultInjectionService,
|
||||
type FaultKind,
|
||||
} from '#/agent/faultInjection/faultInjection';
|
||||
import { IAgentProfileService, type ProfileModelContext } from '#/agent/profile/profile';
|
||||
import { IAgentStateService } from '#/agent/state/agentState';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry';
|
||||
|
|
@ -184,7 +180,6 @@ export class AgentLLMRequesterService implements IAgentLLMRequesterService {
|
|||
@ILogService private readonly log: ILogService,
|
||||
@ITelemetryService private readonly telemetry: ITelemetryService,
|
||||
@IWireService private readonly wire: IWireService,
|
||||
@IFaultInjectionService private readonly faultInjection: IFaultInjectionService,
|
||||
@IEventBus private readonly eventBus: IEventBus,
|
||||
@IAgentStateService private readonly states: IAgentStateService,
|
||||
) {
|
||||
|
|
@ -387,11 +382,6 @@ export class AgentLLMRequesterService implements IAgentLLMRequesterService {
|
|||
this.logRequest(logInput);
|
||||
this.recordRequest(logInput);
|
||||
|
||||
const fault = this.faultInjection.take();
|
||||
if (fault !== undefined) {
|
||||
throw faultToError(fault);
|
||||
}
|
||||
|
||||
let message: Message | undefined;
|
||||
let usage = emptyUsage();
|
||||
let timing: ModelRequestTiming | undefined;
|
||||
|
|
@ -806,12 +796,6 @@ function projectionField(
|
|||
: undefined;
|
||||
}
|
||||
|
||||
function faultToError(kind: FaultKind): Error {
|
||||
return kind === 'request-too-large'
|
||||
? new APIRequestTooLargeError(413, 'Request Entity Too Large (fault injection)')
|
||||
: new APIStatusError(400, 'unsupported image format: image/avif (fault injection)');
|
||||
}
|
||||
|
||||
function fingerprint(content: string): string {
|
||||
return createHash('sha256').update(content).digest('hex');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@
|
|||
* state, so the effort is validated against the model's supported efforts and
|
||||
* the bind rejects up front when unsupported — internal spawns pass inherited
|
||||
* thinking without the flag, and a persisted effort that drifted out of the
|
||||
* model's support list clamps instead of breaking the spawn.
|
||||
* model's support list clamps instead of breaking the spawn. The profile
|
||||
* contract also owns live status re-publication for consumers that attach to
|
||||
* an agent after its initial model binding.
|
||||
*/
|
||||
|
||||
import type {
|
||||
|
|
@ -140,6 +142,7 @@ export interface IAgentProfileService {
|
|||
bind(input: BindAgentInput): Promise<void>;
|
||||
setModel(model: string): Promise<ProfileSetModelResult>;
|
||||
setThinking(level: string): void;
|
||||
republishStatus(): void;
|
||||
getModel(): string;
|
||||
useProfile(profile: ResolvedAgentProfile, context: SystemPromptContext): void;
|
||||
applyProfile(profile: ResolvedAgentProfile, options?: ApplyProfileOptions): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,11 @@
|
|||
* in the synchronous segment before the first dispatch, so concurrent binds
|
||||
* cannot both pass (an edge-level guard always leaves an interleaving
|
||||
* window); a same-name rebind keeps the persisted thinking effort unless the
|
||||
* caller explicitly overrides it. `refreshSystemPrompt` never rejects: a
|
||||
* caller explicitly overrides it. Prompt builds inject the enabled plugins'
|
||||
* system-prompt sections (budget-capped, see `PLUGIN_SECTIONS_MAX_BYTES`);
|
||||
* plugin changes reach the prompt when the session skill catalog re-pulls
|
||||
* its plugin source on explicit plugin reload — the same point where plugin
|
||||
* skills take effect. `refreshSystemPrompt` never rejects: a
|
||||
* failed context build keeps the current prompt and surfaces a warning,
|
||||
* because the `[tools]` config watcher fires it voided (an unhandled
|
||||
* rejection would crash kap-server) and the Session tool-policy fan-out
|
||||
|
|
@ -44,7 +48,7 @@
|
|||
* flag-gated tools (which every builtin profile lists) stay "known" even when
|
||||
* unregistered.
|
||||
* The mutable plain-data state (`activeToolNamesOverlay` / `agentsMdWarning`
|
||||
* / the two emitted-warning dedupe sets) is registered into `agentState`
|
||||
* / the three emitted-warning dedupe sets) is registered into `agentState`
|
||||
* (`IAgentStateService`) and read/written through it; `optionsValue` (holds
|
||||
* the `cwd` / `chdir` / `emitStatusUpdated` callbacks) and `activeProfile`
|
||||
* (a `ResolvedAgentProfile` carrying the `systemPrompt` function) stay plain
|
||||
|
|
@ -86,8 +90,11 @@ import { IHostFileSystem } from '#/os/interface/hostFileSystem';
|
|||
import { ISessionContext } from '#/session/sessionContext/sessionContext';
|
||||
import type { ToolSource } from '#/tool/toolContract';
|
||||
import { ISessionWorkspaceContext } from '#/session/workspaceContext/workspaceContext';
|
||||
import { ISessionSkillCatalog } from '#/session/sessionSkillCatalog/skillCatalog';
|
||||
import { PLUGIN_SKILL_SOURCE_ID } from '#/session/sessionSkillCatalog/pluginSkillSource';
|
||||
import { ISessionAgentProfileCatalog } from '#/session/sessionAgentProfileCatalog/sessionAgentProfileCatalog';
|
||||
import { ISessionToolPolicy } from '#/session/sessionToolPolicy/sessionToolPolicy';
|
||||
import { IPluginService } from '#/app/plugin/plugin';
|
||||
import { IAgentSkillDisclosureService } from '#/agent/skillDisclosure/skillDisclosure';
|
||||
import type { ResolvedAgentProfile, SystemPromptContext } from '#/agent/profile/profile';
|
||||
import { IAgentStateService } from '#/agent/state/agentState';
|
||||
|
|
@ -152,6 +159,8 @@ function describeInactiveToolPattern(
|
|||
}
|
||||
}
|
||||
|
||||
export const PLUGIN_SECTIONS_MAX_BYTES = 64 * 1024;
|
||||
|
||||
export const profileActiveToolNamesOverlayKey = defineState<readonly string[] | undefined>(
|
||||
'profile.activeToolNamesOverlay',
|
||||
() => undefined as readonly string[] | undefined,
|
||||
|
|
@ -168,6 +177,10 @@ export const profileEmittedToolPatternWarningsKey = defineState<Set<string>>(
|
|||
'profile.emittedToolPatternWarnings',
|
||||
() => new Set(),
|
||||
);
|
||||
export const profileEmittedPluginBudgetWarningsKey = defineState<Set<string>>(
|
||||
'profile.emittedPluginBudgetWarnings',
|
||||
() => new Set(),
|
||||
);
|
||||
|
||||
export class AgentProfileService extends Disposable implements IAgentProfileService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
|
@ -198,18 +211,21 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ
|
|||
@IBootstrapService private readonly bootstrap: IBootstrapService,
|
||||
@ISessionWorkspaceContext private readonly workspace: ISessionWorkspaceContext,
|
||||
@ISessionAgentProfileCatalog private readonly catalog: ISessionAgentProfileCatalog,
|
||||
@ISessionSkillCatalog private readonly skillCatalog: ISessionSkillCatalog,
|
||||
@IAgentSkillDisclosureService private readonly skillDisclosure: IAgentSkillDisclosureService,
|
||||
@ISessionToolPolicy private readonly sessionToolPolicy: ISessionToolPolicy,
|
||||
@IAgentToolRegistryService private readonly toolRegistry: IAgentToolRegistryService,
|
||||
@IAgentProfileCatalogService private readonly builtinProfiles: IAgentProfileCatalogService,
|
||||
@IAgentStateService private readonly states: IAgentStateService,
|
||||
@IHostIdentity private readonly hostIdentity: IHostIdentity,
|
||||
@IPluginService private readonly plugins: IPluginService,
|
||||
) {
|
||||
super();
|
||||
this.states.register(profileActiveToolNamesOverlayKey);
|
||||
this.states.register(profileAgentsMdWarningKey);
|
||||
this.states.register(profileEmittedThinkingEffortWarningsKey);
|
||||
this.states.register(profileEmittedToolPatternWarningsKey);
|
||||
this.states.register(profileEmittedPluginBudgetWarningsKey);
|
||||
this.configure({});
|
||||
this._register(
|
||||
this.sessionToolPolicy.onDidChange((event) => {
|
||||
|
|
@ -224,6 +240,13 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ
|
|||
}
|
||||
}),
|
||||
);
|
||||
this._register(
|
||||
this.skillCatalog.onDidChange((sourceId) => {
|
||||
if (sourceId === PLUGIN_SKILL_SOURCE_ID) {
|
||||
void this.refreshSystemPrompt();
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private get activeToolNamesOverlay(): readonly string[] | undefined {
|
||||
|
|
@ -250,6 +273,10 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ
|
|||
return this.states.get(profileEmittedToolPatternWarningsKey);
|
||||
}
|
||||
|
||||
private get emittedPluginBudgetWarnings(): Set<string> {
|
||||
return this.states.get(profileEmittedPluginBudgetWarningsKey);
|
||||
}
|
||||
|
||||
configure(options: ProfileServiceOptions): void {
|
||||
this.optionsValue = {
|
||||
cwd: options.cwd ?? this.optionsValue.cwd,
|
||||
|
|
@ -691,6 +718,10 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ
|
|||
});
|
||||
}
|
||||
|
||||
republishStatus(): void {
|
||||
this.emitStatusUpdated(true);
|
||||
}
|
||||
|
||||
private get profileState(): ProfileModelState {
|
||||
return this.wire.getModel(ProfileModel);
|
||||
}
|
||||
|
|
@ -888,6 +919,7 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ
|
|||
);
|
||||
const skillActive = this.isToolActiveForProfile(profile, 'Skill');
|
||||
const skillDisclosure = await this.skillDisclosure.resolve(skillActive);
|
||||
const pluginSections = await this.resolvePluginSections();
|
||||
return {
|
||||
...base,
|
||||
cwd: effectiveCwd,
|
||||
|
|
@ -898,6 +930,7 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ
|
|||
skills: skillDisclosure.listing,
|
||||
skillActive,
|
||||
disclosedSkillNames: skillDisclosure.names,
|
||||
pluginSections,
|
||||
productName: this.hostIdentity.productName,
|
||||
replyStyleGuide: this.hostIdentity.replyStyleGuide,
|
||||
};
|
||||
|
|
@ -928,6 +961,37 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ
|
|||
}
|
||||
}
|
||||
|
||||
private async resolvePluginSections(): Promise<string> {
|
||||
const sections = await this.plugins.enabledSystemPrompts();
|
||||
const parts: string[] = [];
|
||||
const skipped: string[] = [];
|
||||
let totalBytes = 0;
|
||||
for (const section of sections) {
|
||||
const block = `<!-- From: plugin ${section.pluginId} -->\n${section.content}`;
|
||||
const bytes = Buffer.byteLength(block, 'utf8');
|
||||
if (totalBytes + bytes > PLUGIN_SECTIONS_MAX_BYTES) {
|
||||
skipped.push(section.pluginId);
|
||||
continue;
|
||||
}
|
||||
totalBytes += bytes;
|
||||
parts.push(block);
|
||||
}
|
||||
if (skipped.length > 0) {
|
||||
const newlySkipped = skipped.filter((id) => !this.emittedPluginBudgetWarnings.has(id));
|
||||
if (newlySkipped.length > 0) {
|
||||
for (const id of newlySkipped) this.emittedPluginBudgetWarnings.add(id);
|
||||
this.eventBus.publish({
|
||||
type: 'warning',
|
||||
message:
|
||||
`Plugin system-prompt contributions from ${newlySkipped.map((id) => `"${id}"`).join(', ')} ` +
|
||||
`were skipped: the aggregate ${PLUGIN_SECTIONS_MAX_BYTES / 1024} KB budget is exhausted.`,
|
||||
code: 'plugin-sections-oversized',
|
||||
});
|
||||
}
|
||||
}
|
||||
return parts.join('\n\n');
|
||||
}
|
||||
|
||||
private readConfiguredCwd(): string | undefined {
|
||||
const cwd = this.optionsValue.cwd;
|
||||
return typeof cwd === 'function' ? cwd() : cwd;
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ const NOTIFICATION_FALLBACK_PREVIEW_BYTES = 3_000;
|
|||
const ACTIVE_BACKGROUND_TASK_INJECTION_VARIANT = 'background_task_status';
|
||||
const ACTIVE_BACKGROUND_TASK_GUIDANCE = [
|
||||
'The conversation was compacted, so the earlier messages that started these background tasks are gone — but the tasks are still running from before.',
|
||||
'Do not start duplicates. Use TaskOutput to fetch a task’s result, TaskList to list them, and TaskStop to cancel one.',
|
||||
'Do not start duplicates. Use TaskList to list them, TaskOutput for a non-blocking status/output snapshot, and TaskStop to cancel one — completion arrives via automatic notification.',
|
||||
].join(' ');
|
||||
|
||||
export function isAgentTaskTerminal(status: AgentTaskStatus): boolean {
|
||||
|
|
|
|||
|
|
@ -1,3 +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 block=true`, sleeping, or otherwise): that just blocks the turn for no benefit — run it in the foreground instead.
|
||||
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 (by polling `TaskOutput`, sleeping, or otherwise): that just blocks the turn for no benefit — run it in the foreground instead.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ The dedicated tools render in the per-tool permission UI and keep raw stdout out
|
|||
**Output:**
|
||||
The stdout and stderr will be combined and returned as a string. The output may be truncated if it is too long. If the command exits non-zero, the output ends with a `Command failed with exit code: N` line; a command killed by its timeout or interrupted by the user ends with its own message instead.
|
||||
|
||||
If `run_in_background=true`, the command will be started as a background task and this tool will return a task ID instead of waiting for command completion. When doing that, you must provide a short `description`. Background commands default to a ${DEFAULT_BACKGROUND_TIMEOUT_S}s timeout and `timeout` is capped at ${MAX_BACKGROUND_TIMEOUT_S}s; set `disable_timeout=true` only when the task should run without a timeout. You will be automatically notified when the task completes. After starting one, default to returning control to the user instead of immediately waiting on it. Use `TaskOutput` only for a non-blocking status/output snapshot — do not set `block=true` to wait for a task you just launched, since its completion arrives automatically; reserve `block=true` for when the user explicitly asked you to wait. Use `TaskStop` only if the task must be cancelled. If a human user wants to inspect background tasks themselves, point them to the `/tasks` command, which opens an interactive panel; it has no subcommands.
|
||||
If `run_in_background=true`, the command will be started as a background task and this tool will return a task ID instead of waiting for command completion. When doing that, you must provide a short `description`. Background commands default to a ${DEFAULT_BACKGROUND_TIMEOUT_S}s timeout and `timeout` is capped at ${MAX_BACKGROUND_TIMEOUT_S}s; set `disable_timeout=true` only when the task should run without a timeout. You will be automatically notified when the task completes. After starting one, default to returning control to the user instead of immediately waiting on it. Use `TaskOutput` only for a non-blocking status/output snapshot — do not wait on a task you just launched, since its completion arrives automatically. Use `TaskStop` only if the task must be cancelled. If a human user wants to inspect background tasks themselves, point them to the `/tasks` command, which opens an interactive panel; it has no subcommands.
|
||||
|
||||
**Guidelines for safety and security:**
|
||||
- Each shell tool call will be executed in a fresh shell environment. The shell variables, current working directory changes, and the shell history is not preserved between calls. To run a command in a particular directory, pass the `cwd` argument (or use absolute paths) rather than relying on a `cd` from an earlier call.
|
||||
|
|
|
|||
|
|
@ -385,7 +385,7 @@ export class BashTool implements IBashTool {
|
|||
if (!output.fullOutputAvailable || output.outputPath === undefined) return result;
|
||||
|
||||
const taskOutputHint = this.allowBackground()
|
||||
? `, or TaskOutput(task_id="${taskId}", block=false)`
|
||||
? `, or TaskOutput(task_id="${taskId}")`
|
||||
: '';
|
||||
const reference =
|
||||
`\n\n[Full output saved]\n` +
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
Retrieve a snapshot of a running or completed background task.
|
||||
|
||||
Use this after `Bash(run_in_background=true)` or `Agent(run_in_background=true)` to check progress, or to read the output of a task that has already completed.
|
||||
Use this after `Bash(run_in_background=true)`, `Agent(run_in_background=true)`, or `AskUserQuestion(background=true)` to check progress, or to read the output of a task that has already completed.
|
||||
|
||||
Guidelines:
|
||||
- Prefer relying on automatic completion notifications. Use this tool only when you need task output before the automatic notification arrives.
|
||||
- By default this tool is non-blocking and returns a current status/output snapshot — that is the normal way to use it.
|
||||
- This tool is always non-blocking: it returns the current status/output snapshot immediately and never waits for the task to finish.
|
||||
- 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.
|
||||
- Use block=true only when the user explicitly asked you to wait for the task. Never block on a task you launched in the current turn — if you need its result right away, it should have been a foreground call.
|
||||
- If a block=true call returns `retrieval_status: timeout` (the task is still running), do not block on the same task again. Continue with other work or hand back to the user — the completion notification arrives on its own.
|
||||
- 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. 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.
|
||||
|
|
|
|||
|
|
@ -15,21 +15,6 @@ import { type AgentTool } from '#/tool/toolContract';
|
|||
|
||||
export const TaskOutputInputSchema = z.object({
|
||||
task_id: z.string().describe('The background task ID to inspect.'),
|
||||
block: z
|
||||
.boolean()
|
||||
.default(false)
|
||||
.describe(
|
||||
'Whether to wait for the task to finish before returning. Discouraged — background tasks notify automatically on completion; use only when the user explicitly asked you to wait.',
|
||||
)
|
||||
.optional(),
|
||||
timeout: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(3600)
|
||||
.default(30)
|
||||
.describe('Maximum number of seconds to wait when block=true.')
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type TaskOutputInput = z.infer<typeof TaskOutputInputSchema>;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue