diff --git a/.agents/skills/agent-core-dev/flags.md b/.agents/skills/agent-core-dev/flags.md index e4c33d8b2..ba4ea424b 100644 --- a/.agents/skills/agent-core-dev/flags.md +++ b/.agents/skills/agent-core-dev/flags.md @@ -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//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//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 diff --git a/.changeset/plugin-system-prompt-section.md b/.changeset/plugin-system-prompt-section.md deleted file mode 100644 index 21869deec..000000000 --- a/.changeset/plugin-system-prompt-section.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@moonshot-ai/kimi-code": minor ---- - -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`). diff --git a/.changeset/v1-custom-agent-files.md b/.changeset/v1-custom-agent-files.md deleted file mode 100644 index 94b79e89a..000000000 --- a/.changeset/v1-custom-agent-files.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@moonshot-ai/kimi-code": minor ---- - -Support Markdown-defined custom agents on agent-core. diff --git a/.changeset/v1-secondary-model.md b/.changeset/v1-secondary-model.md deleted file mode 100644 index 181ba6978..000000000 --- a/.changeset/v1-secondary-model.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@moonshot-ai/kimi-code": minor ---- - -Add the /secondary_model slash command to configure the secondary model used by subagents. diff --git a/apps/kimi-code/CHANGELOG.md b/apps/kimi-code/CHANGELOG.md index 8b17b4c2f..4b6276383 100644 --- a/apps/kimi-code/CHANGELOG.md +++ b/apps/kimi-code/CHANGELOG.md @@ -1,5 +1,25 @@ # @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 diff --git a/apps/kimi-code/package.json b/apps/kimi-code/package.json index cf6ae7f62..f78eac615 100644 --- a/apps/kimi-code/package.json +++ b/apps/kimi-code/package.json @@ -1,6 +1,6 @@ { "name": "@moonshot-ai/kimi-code", - "version": "0.30.0", + "version": "0.31.0", "description": "The Starting Point for Next-Gen Agents", "license": "MIT", "author": "Moonshot AI", diff --git a/apps/kimi-code/src/cli/sub/web/run.ts b/apps/kimi-code/src/cli/sub/web/run.ts index 9318b8539..26088c05f 100644 --- a/apps/kimi-code/src/cli/sub/web/run.ts +++ b/apps/kimi-code/src/cli/sub/web/run.ts @@ -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'); diff --git a/apps/kimi-code/src/cli/v2/run-v2-print.ts b/apps/kimi-code/src/cli/v2/run-v2-print.ts index 0bf117b03..3f8ccbcf7 100644 --- a/apps/kimi-code/src/cli/v2/run-v2-print.ts +++ b/apps/kimi-code/src/cli/v2/run-v2-print.ts @@ -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 diff --git a/apps/kimi-code/src/cli/version.ts b/apps/kimi-code/src/cli/version.ts index d5a4f36cd..21e121393 100644 --- a/apps/kimi-code/src/cli/version.ts +++ b/apps/kimi-code/src/cli/version.ts @@ -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 { - return createKimiDefaultHeaders({ - homeDir: getDataDir(), - ...createKimiCodeHostIdentity(version), - }); -} diff --git a/apps/kimi-code/src/constant/app.ts b/apps/kimi-code/src/constant/app.ts index 59aa18797..b7cd74939 100644 --- a/apps/kimi-code/src/constant/app.ts +++ b/apps/kimi-code/src/constant/app.ts @@ -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/ (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; diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 02cf0494b..79c9f1d7d 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -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, @@ -1684,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; } diff --git a/apps/kimi-code/test/cli/run-shell.test.ts b/apps/kimi-code/test/cli/run-shell.test.ts index 7e6260626..8f0c17d9d 100644 --- a/apps/kimi-code/test/cli/run-shell.test.ts +++ b/apps/kimi-code/test/cli/run-shell.test.ts @@ -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 }, diff --git a/apps/kimi-code/test/cli/v2-run-print.test.ts b/apps/kimi-code/test/cli/v2-run-print.test.ts index e6a5e5ce3..27c664389 100644 --- a/apps/kimi-code/test/cli/v2-run-print.test.ts +++ b/apps/kimi-code/test/cli/v2-run-print.test.ts @@ -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, }, diff --git a/apps/kimi-code/test/cli/version.test.ts b/apps/kimi-code/test/cli/version.test.ts index 073d5c727..545126220 100644 --- a/apps/kimi-code/test/cli/version.test.ts +++ b/apps/kimi-code/test/cli/version.test.ts @@ -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'); }); diff --git a/apps/vscode/CHANGELOG.md b/apps/vscode/CHANGELOG.md index 795479752..4aa5eb5e0 100644 --- a/apps/vscode/CHANGELOG.md +++ b/apps/vscode/CHANGELOG.md @@ -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 diff --git a/apps/vscode/docs/node-sdk-migration.md b/apps/vscode/docs/node-sdk-migration.md index 92ba893d0..78a973165 100644 --- a/apps/vscode/docs/node-sdk-migration.md +++ b/apps/vscode/docs/node-sdk-migration.md @@ -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"` diff --git a/apps/vscode/package.json b/apps/vscode/package.json index 5af04b7a9..702363603 100644 --- a/apps/vscode/package.json +++ b/apps/vscode/package.json @@ -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", diff --git a/apps/vscode/scripts/vsix-verify.mjs b/apps/vscode/scripts/vsix-verify.mjs index b1e05219e..ee81434cc 100644 --- a/apps/vscode/scripts/vsix-verify.mjs +++ b/apps/vscode/scripts/vsix-verify.mjs @@ -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'; } diff --git a/apps/vscode/src/runtime/kimi-runtime.ts b/apps/vscode/src/runtime/kimi-runtime.ts index 9c5a9e544..d07af86f8 100644 --- a/apps/vscode/src/runtime/kimi-runtime.ts +++ b/apps/vscode/src/runtime/kimi-runtime.ts @@ -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", }); diff --git a/apps/vscode/test/app-init.test.ts b/apps/vscode/test/app-init.test.ts new file mode 100644 index 000000000..85393df14 --- /dev/null +++ b/apps/vscode/test/app-init.test.ts @@ -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 }); + } + }); +}); diff --git a/apps/vscode/test/kimi-harness.integration.test.ts b/apps/vscode/test/kimi-harness.integration.test.ts index 21c466b84..ea53fae3d 100644 --- a/apps/vscode/test/kimi-harness.integration.test.ts +++ b/apps/vscode/test/kimi-harness.integration.test.ts @@ -141,7 +141,7 @@ async function createRuntimeRig(extraAliases: readonly string[] = []): Promise { 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; diff --git a/apps/vscode/test/replay-resume.integration.test.ts b/apps/vscode/test/replay-resume.integration.test.ts index 01706ea44..ab0d146c5 100644 --- a/apps/vscode/test/replay-resume.integration.test.ts +++ b/apps/vscode/test/replay-resume.integration.test.ts @@ -46,7 +46,7 @@ async function createReplayRig(): Promise { 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: { diff --git a/apps/vscode/test/vsix-package.test.ts b/apps/vscode/test/vsix-package.test.ts index dc1ccb30b..bd258e7a7 100644 --- a/apps/vscode/test/vsix-package.test.ts +++ b/apps/vscode/test/vsix-package.test.ts @@ -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'); diff --git a/apps/vscode/tsconfig.json b/apps/vscode/tsconfig.json index bc9d28463..2e8486a7d 100644 --- a/apps/vscode/tsconfig.json +++ b/apps/vscode/tsconfig.json @@ -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"] } diff --git a/apps/vscode/webview-ui/src/App.tsx b/apps/vscode/webview-ui/src/App.tsx index 4a343fe89..2c06bb29a 100644 --- a/apps/vscode/webview-ui/src/App.tsx +++ b/apps/vscode/webview-ui/src/App.tsx @@ -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 (
@@ -106,23 +118,17 @@ export default function App() { ); } - // 跳过登录但没有模型 - if (skippedLogin && modelsCount === 0) { + // 错误与设置状态界面;no-models 必须保留回到登录界面的入口 + if (resolution.view === "status") { return (
- setSkippedLogin(false)} /> - -
- ); - } - - // 其他错误状态 - if (status !== "ready" && status !== "not-logged-in") { - return ( -
-
- +
); diff --git a/apps/vscode/webview-ui/src/components/LoginScreen.tsx b/apps/vscode/webview-ui/src/components/LoginScreen.tsx index f572abc7e..07fff46b4 100644 --- a/apps/vscode/webview-ui/src/components/LoginScreen.tsx +++ b/apps/vscode/webview-ui/src/components/LoginScreen.tsx @@ -89,7 +89,7 @@ export function LoginScreen({ onLoginSuccess, onSkip }: LoginScreenProps) {
- Waiting for authorization... + Waiting for authentication...

A browser window should open automatically. Complete the sign-in process there.

diff --git a/apps/vscode/webview-ui/src/hooks/useAppInit.ts b/apps/vscode/webview-ui/src/hooks/useAppInit.ts index f23f2d530..c592b8b7e 100644 --- a/apps/vscode/webview-ui/src/hooks/useAppInit.ts +++ b/apps/vscode/webview-ui/src/hooks/useAppInit.ts @@ -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; diff --git a/apps/vscode/webview-ui/tsconfig.json b/apps/vscode/webview-ui/tsconfig.json index 4e59bb53e..8ed301566 100644 --- a/apps/vscode/webview-ui/tsconfig.json +++ b/apps/vscode/webview-ui/tsconfig.json @@ -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"] } diff --git a/docs/en/customization/agents.md b/docs/en/customization/agents.md index 6aab183df..b9c67039d 100644 --- a/docs/en/customization/agents.md +++ b/docs/en/customization/agents.md @@ -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 diff --git a/docs/en/customization/plugins.md b/docs/en/customization/plugins.md index b67956525..80f9a0738 100644 --- a/docs/en/customization/plugins.md +++ b/docs/en/customization/plugins.md @@ -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, 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. +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 @@ -162,6 +162,7 @@ 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 | @@ -271,6 +272,19 @@ my-plugin/ Regardless of how a Skill is loaded (`sessionStart.skill`, `/skill:`, 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. diff --git a/docs/en/reference/tools.md b/docs/en/reference/tools.md index 0c38fd486..8b412b536 100644 --- a/docs/en/reference/tools.md +++ b/docs/en/reference/tools.md @@ -109,7 +109,7 @@ 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. diff --git a/docs/en/release-notes/changelog.md b/docs/en/release-notes/changelog.md index a1b797fc8..84ba624a1 100644 --- a/docs/en/release-notes/changelog.md +++ b/docs/en/release-notes/changelog.md @@ -6,6 +6,21 @@ 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 diff --git a/docs/zh/customization/agents.md b/docs/zh/customization/agents.md index 56bdbd95a..9a935e94f 100644 --- a/docs/zh/customization/agents.md +++ b/docs/zh/customization/agents.md @@ -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 信任模型 diff --git a/docs/zh/customization/plugins.md b/docs/zh/customization/plugins.md index 33fb08cd3..f00070971 100644 --- a/docs/zh/customization/plugins.md +++ b/docs/zh/customization/plugins.md @@ -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 安装扩展。 ## 安装与管理 @@ -162,6 +162,7 @@ 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 系统提示词的内联指令 | @@ -271,6 +272,19 @@ my-plugin/ 无论 Skill 通过哪种方式加载(`sessionStart.skill`、`/skill:` 或模型自动调用),`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。 diff --git a/docs/zh/reference/tools.md b/docs/zh/reference/tools.md index 04e84a6ad..009ff3d05 100644 --- a/docs/zh/reference/tools.md +++ b/docs/zh/reference/tools.md @@ -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`)。对已处于终止状态的任务也能安全调用。 diff --git a/docs/zh/release-notes/changelog.md b/docs/zh/release-notes/changelog.md index 2b813a801..8dd7ad636 100644 --- a/docs/zh/release-notes/changelog.md +++ b/docs/zh/release-notes/changelog.md @@ -6,6 +6,21 @@ 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) ### 新功能 diff --git a/packages/acp-adapter/CHANGELOG.md b/packages/acp-adapter/CHANGELOG.md index 9cee6ab56..eaca59808 100644 --- a/packages/acp-adapter/CHANGELOG.md +++ b/packages/acp-adapter/CHANGELOG.md @@ -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 diff --git a/packages/acp-adapter/package.json b/packages/acp-adapter/package.json index 753bb3b9e..0235d35db 100644 --- a/packages/acp-adapter/package.json +++ b/packages/acp-adapter/package.json @@ -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", diff --git a/packages/agent-core-v2/CHANGELOG.md b/packages/agent-core-v2/CHANGELOG.md index 6b9a0fb68..a4cbfaaaa 100644 --- a/packages/agent-core-v2/CHANGELOG.md +++ b/packages/agent-core-v2/CHANGELOG.md @@ -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 diff --git a/packages/agent-core-v2/docs/state-manifest.d.ts b/packages/agent-core-v2/docs/state-manifest.d.ts index a64f61b71..750881c5c 100644 --- a/packages/agent-core-v2/docs/state-manifest.d.ts +++ b/packages/agent-core-v2/docs/state-manifest.d.ts @@ -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: 67 keys) // Session // cron.inFlight src/session/cron/sessionCronServiceImpl.ts // cron.lastSeenAt src/session/cron/sessionCronServiceImpl.ts @@ -63,8 +63,6 @@ // contextProjector.lastRepairSignature src/agent/contextProjector/contextProjectorService.ts // contextSize.lastEmittedTokens src/agent/contextSize/contextSizeService.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 @@ -983,9 +981,6 @@ export interface AgentStateSnapshot { 'contextSize.lastEmittedTokens': number; // 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; @@ -1013,7 +1008,7 @@ export interface AgentStateSnapshot { 'llmRequester.lastConfigLogSignature': string | undefined; 'llmRequester.mediaDegradedTurns': Set; 'llmRequester.mediaStrippedTurns': Map; 'llmRequester.turnConfigs': Map = - createDecorator('faultInjectionService'); diff --git a/packages/agent-core-v2/src/agent/faultInjection/faultInjectionService.ts b/packages/agent-core-v2/src/agent/faultInjection/faultInjectionService.ts deleted file mode 100644 index b4c7417df..000000000 --- a/packages/agent-core-v2/src/agent/faultInjection/faultInjectionService.ts +++ /dev/null @@ -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( - 'faultInjection.armed', - () => undefined as FaultKind | undefined, -); -export const faultInjectionFiredKey = defineState('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', -); diff --git a/packages/agent-core-v2/src/agent/faultInjection/flag.ts b/packages/agent-core-v2/src/agent/faultInjection/flag.ts deleted file mode 100644 index c3eaaee9a..000000000 --- a/packages/agent-core-v2/src/agent/faultInjection/flag.ts +++ /dev/null @@ -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); diff --git a/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts b/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts index 057bebd88..ccd121d88 100644 --- a/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts +++ b/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts @@ -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'); } diff --git a/packages/agent-core-v2/src/agent/task/taskService.ts b/packages/agent-core-v2/src/agent/task/taskService.ts index cc454d3de..b81ebb938 100644 --- a/packages/agent-core-v2/src/agent/task/taskService.ts +++ b/packages/agent-core-v2/src/agent/task/taskService.ts @@ -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 { diff --git a/packages/agent-core-v2/src/agent/tools/agent/agent-background-enabled.md b/packages/agent-core-v2/src/agent/tools/agent/agent-background-enabled.md index 67cafb5b7..7c2c65f35 100644 --- a/packages/agent-core-v2/src/agent/tools/agent/agent-background-enabled.md +++ b/packages/agent-core-v2/src/agent/tools/agent/agent-background-enabled.md @@ -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. diff --git a/packages/agent-core-v2/src/agent/tools/os/bash/bash.md b/packages/agent-core-v2/src/agent/tools/os/bash/bash.md index 13fc46b91..6b3ec9c4b 100644 --- a/packages/agent-core-v2/src/agent/tools/os/bash/bash.md +++ b/packages/agent-core-v2/src/agent/tools/os/bash/bash.md @@ -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. diff --git a/packages/agent-core-v2/src/agent/tools/os/bash/bashTool.ts b/packages/agent-core-v2/src/agent/tools/os/bash/bashTool.ts index 88c3bb666..0cda986a2 100644 --- a/packages/agent-core-v2/src/agent/tools/os/bash/bashTool.ts +++ b/packages/agent-core-v2/src/agent/tools/os/bash/bashTool.ts @@ -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` + diff --git a/packages/agent-core-v2/src/agent/tools/task/task-output/task-output.md b/packages/agent-core-v2/src/agent/tools/task/task-output/task-output.md index 7b902943d..7e53c035d 100644 --- a/packages/agent-core-v2/src/agent/tools/task/task-output/task-output.md +++ b/packages/agent-core-v2/src/agent/tools/task/task-output/task-output.md @@ -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. diff --git a/packages/agent-core-v2/src/agent/tools/task/task-output/task-output.ts b/packages/agent-core-v2/src/agent/tools/task/task-output/task-output.ts index aa3c00eae..977db3542 100644 --- a/packages/agent-core-v2/src/agent/tools/task/task-output/task-output.ts +++ b/packages/agent-core-v2/src/agent/tools/task/task-output/task-output.ts @@ -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; diff --git a/packages/agent-core-v2/src/agent/tools/task/task-output/taskOutputTool.ts b/packages/agent-core-v2/src/agent/tools/task/task-output/taskOutputTool.ts index 063a45f91..d5983a171 100644 --- a/packages/agent-core-v2/src/agent/tools/task/task-output/taskOutputTool.ts +++ b/packages/agent-core-v2/src/agent/tools/task/task-output/taskOutputTool.ts @@ -39,12 +39,8 @@ const OUTPUT_PREVIEW_BYTES = 32 * 1024; const PAGING_HINT_LINES = 300; -function retrievalStatus( - status: AgentTaskStatus, - block: boolean | undefined, -): 'success' | 'timeout' | 'not_ready' { - if (TERMINAL_STATUSES.has(status)) return 'success'; - return block ? 'timeout' : 'not_ready'; +function retrievalStatus(status: AgentTaskStatus): 'success' | 'not_ready' { + return TERMINAL_STATUSES.has(status) ? 'success' : 'not_ready'; } function terminalReason(info: AgentTaskInfo): 'timed_out' | 'stopped' | 'failed' | undefined { @@ -85,23 +81,11 @@ export class TaskOutputTool implements ITaskOutputTool { description: `Reading output of task ${args.task_id}`, approvalRule: this.name, matchesRule: (ruleArgs) => matchesGlobRuleSubject(ruleArgs, args.task_id), - execute: ({ signal }) => this.execute(args, signal), + execute: () => this.execute(args), }; } - private async execute( - args: TaskOutputInput, - signal: AbortSignal, - ): Promise { - const info = this.tasks.getTask(args.task_id); - if (!info) { - return { isError: true, output: `Task not found: ${args.task_id}` }; - } - - if (args.block && !TERMINAL_STATUSES.has(info.status)) { - await this.tasks.wait(args.task_id, (args.timeout ?? 30) * 1000, signal); - } - + private async execute(args: TaskOutputInput): Promise { const current = this.tasks.getTask(args.task_id); if (!current) { return { isError: true, output: `Task not found: ${args.task_id}` }; @@ -111,7 +95,7 @@ export class TaskOutputTool implements ITaskOutputTool { const lines = [ formatPlainObject({ - retrievalStatus: retrievalStatus(current.status, args.block), + retrievalStatus: retrievalStatus(current.status), ...current, outputPath: output.outputPath, terminalReason: terminalReason(current), @@ -122,10 +106,6 @@ export class TaskOutputTool implements ITaskOutputTool { fullOutputTool: output.fullOutputAvailable && output.outputPath !== undefined ? 'Read' : undefined, fullOutputHint: fullOutputHint(output), - nextStep: - args.block === true && !TERMINAL_STATUSES.has(current.status) - ? 'The task is still running after waiting. Do not block on it again — continue with other work or hand back to the user; you will be notified automatically when it completes.' - : undefined, }), '', ]; diff --git a/packages/agent-core-v2/src/app/agentFileCatalog/agentProfileSource.ts b/packages/agent-core-v2/src/app/agentFileCatalog/agentProfileSource.ts index 70e9daa6a..3fa3661e9 100644 --- a/packages/agent-core-v2/src/app/agentFileCatalog/agentProfileSource.ts +++ b/packages/agent-core-v2/src/app/agentFileCatalog/agentProfileSource.ts @@ -7,9 +7,9 @@ * collisions). Mirrors `skillCatalog/skillSource`, with one deliberate * deviation: `explicit` outranks every other source (in the skill system it * aliases `user`) because `--agent-file` is a one-shot command-line intent that - * must always win. Concrete sources (user at App scope; project / extra / - * explicit at Session scope) each bind their own DI token extending this - * contract. + * must always win. Concrete sources (user at App scope; plugin / project / + * extra / explicit at Session scope) each bind their own DI token extending + * this contract. * * A source may mark `load()` failures as `fatal`: the Session catalog lets * them propagate into `ready` so awaiters see the error (`explicit` does — @@ -38,6 +38,7 @@ export interface AgentProfileContribution { } export const AGENT_PROFILE_SOURCE_PRIORITY = { + plugin: 5, user: 10, extra: 20, project: 30, diff --git a/packages/agent-core-v2/src/app/agentFileCatalog/types.ts b/packages/agent-core-v2/src/app/agentFileCatalog/types.ts index 7203f9b95..9187fd2a3 100644 --- a/packages/agent-core-v2/src/app/agentFileCatalog/types.ts +++ b/packages/agent-core-v2/src/app/agentFileCatalog/types.ts @@ -9,7 +9,7 @@ import type { AgentModelPreference } from '#/app/agentProfileCatalog/agentProfileCatalog'; -export type AgentFileSource = 'project' | 'user' | 'extra' | 'explicit'; +export type AgentFileSource = 'plugin' | 'project' | 'user' | 'extra' | 'explicit'; export interface AgentFileRoot { readonly path: string; diff --git a/packages/agent-core-v2/src/app/auth/auth.ts b/packages/agent-core-v2/src/app/auth/auth.ts index b55677003..812b32677 100644 --- a/packages/agent-core-v2/src/app/auth/auth.ts +++ b/packages/agent-core-v2/src/app/auth/auth.ts @@ -11,6 +11,7 @@ */ import type { + AuthManagedUserInfoResult, AuthManagedUsageResult, BearerTokenProvider, KimiOAuthLoginOptions, @@ -47,6 +48,7 @@ export interface IOAuthService { status(provider?: string): Promise; refreshOAuthProviderModels(): Promise; getManagedUsage(provider?: string): Promise; + getManagedUserInfo(provider?: string): Promise; resolveTokenProvider(provider: string, oauthRef?: OAuthRef): BearerTokenProvider | undefined; getCachedAccessToken(provider: string, oauthRef?: OAuthRef): Promise; } @@ -68,6 +70,10 @@ export interface IOAuthToolkit { providerName?: string, options?: { readonly oauthRef?: KimiOAuthTokenRef; readonly baseUrl?: string }, ): Promise; + getManagedUserInfo( + providerName?: string, + options?: { readonly oauthRef?: KimiOAuthTokenRef; readonly baseUrl?: string }, + ): Promise; } export const IOAuthToolkit: ServiceIdentifier = diff --git a/packages/agent-core-v2/src/app/auth/authService.ts b/packages/agent-core-v2/src/app/auth/authService.ts index 71b6c71c2..b6a5fb4e0 100644 --- a/packages/agent-core-v2/src/app/auth/authService.ts +++ b/packages/agent-core-v2/src/app/auth/authService.ts @@ -27,6 +27,7 @@ import { resolveKimiCodeLoginAuth, resolveKimiCodeOAuthRef, resolveKimiCodeRuntimeAuth, + type AuthManagedUserInfoResult, type AuthManagedUsageResult, type BearerTokenProvider, type DeviceAuthorization, @@ -279,6 +280,18 @@ export class OAuthService extends Disposable implements IOAuthService { }); } + getManagedUserInfo(provider = KIMI_CODE_PROVIDER_NAME): Promise { + const configured = this.providerService.get(provider); + const auth = resolveKimiCodeRuntimeAuth({ + configuredBaseUrl: configured?.baseUrl, + configuredOAuthRef: configured?.oauth, + }); + return this.toolkit.getManagedUserInfo(provider, { + oauthRef: auth.oauthRef, + baseUrl: auth.baseUrl, + }); + } + refreshOAuthProviderModels(): Promise { const run = this.refreshChain.then(() => this.doRefreshOAuthProviderModels()); this.refreshChain = run.then( @@ -864,7 +877,7 @@ function managedModel( class OAuthToolkitService extends KimiOAuthToolkit implements IOAuthToolkit { declare readonly _serviceBrand: undefined; constructor(@IBootstrapService bootstrap: IBootstrapService) { - super({ homeDir: bootstrap.homeDir }); + super({ homeDir: bootstrap.homeDir, identity: bootstrap.clientIdentity }); } } diff --git a/packages/agent-core-v2/src/app/auth/oauthProtocol.ts b/packages/agent-core-v2/src/app/auth/oauthProtocol.ts index 106421720..e3bc88829 100644 --- a/packages/agent-core-v2/src/app/auth/oauthProtocol.ts +++ b/packages/agent-core-v2/src/app/auth/oauthProtocol.ts @@ -4,6 +4,8 @@ * Request/response shapes of the v1 `/oauth/*` endpoints plus the managed * OAuth provider model-refresh response, defined as zod schemas so the * transports validate against the same contract the `IOAuthService` returns. + * New endpoints use the camelCase domain contract owned by the oauth + * package (re-exported below); legacy snake_case schemas stay local. */ import { z } from 'zod'; @@ -158,3 +160,15 @@ export const managedUsageResultSchema = z.discriminatedUnion('kind', [ managedUsageErrorSchema, ]); export type ManagedUsageResult = z.infer; + +// --------------------------------------------------------------------------- +// Managed-account profile (`GET /v1/oauth/userinfo`) — camelCase domain +// contract owned by `@moonshot-ai/kimi-code-oauth` (its zod schemas are the +// single source of truth); re-exported here so transports keep one import +// path. Legacy snake_case endpoints keep their local schemas above. +// --------------------------------------------------------------------------- + +export { + managedUserInfoResultSchema, + type ManagedUserInfoResult, +} from '@moonshot-ai/kimi-code-oauth'; diff --git a/packages/agent-core-v2/src/app/bootstrap/bootstrap.ts b/packages/agent-core-v2/src/app/bootstrap/bootstrap.ts index 0ea371c39..509b9566d 100644 --- a/packages/agent-core-v2/src/app/bootstrap/bootstrap.ts +++ b/packages/agent-core-v2/src/app/bootstrap/bootstrap.ts @@ -3,7 +3,7 @@ * * Defines the `IBootstrapService`, the snapshot of the world the process runs * in, resolved once at startup and frozen for the process: observed host facts - * (`platform`, `arch`, `cwd`, `osHomeDir`, `getEnv`, `clientVersion`) and the + * (`platform`, `arch`, `cwd`, `osHomeDir`, `getEnv`, `clientIdentity`) and the * app path layout (`homeDir`, `configPath`, …). `resolveBootstrapOptions` is * the single place that reads `process.env` / `os.homedir()` / invocation * input to resolve the snapshot; everything downstream reads from @@ -18,6 +18,8 @@ import { homedir } from 'node:os'; import { join } from 'pathe'; +import type { KimiHostIdentity } from '@moonshot-ai/kimi-code-oauth'; + import { SyncDescriptor } from '#/_base/di/descriptors'; import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation'; import { createAppScope, type Scope, type ScopeSeed } from '#/_base/di/scope'; @@ -36,7 +38,7 @@ export interface IBootstrapOptions { readonly arch: string; readonly cwd: string; readonly env: NodeJS.ProcessEnv; - readonly clientVersion: string; + readonly clientIdentity: KimiHostIdentity; } export const IBootstrapOptions: ServiceIdentifier = @@ -61,7 +63,7 @@ export interface IBootstrapService { readonly osHomeDir: string; readonly homeDir: string; readonly configPath: string; - readonly clientVersion: string; + readonly clientIdentity: KimiHostIdentity; readonly sessionsDir: string; readonly blobsDir: string; readonly storeDir: string; @@ -87,10 +89,12 @@ export interface BootstrapInput { readonly platform?: NodeJS.Platform; readonly arch?: string; readonly cwd?: string; - readonly clientVersion?: string; + /** Required: every process names its host. There is deliberately no default + — a fabricated identity would silently misreport the host upstream. */ + readonly clientIdentity: KimiHostIdentity; } -export function resolveBootstrapOptions(input: BootstrapInput = {}): IBootstrapOptions { +export function resolveBootstrapOptions(input: BootstrapInput): IBootstrapOptions { const env = input.env ?? process.env; const osHomeDir = input.osHomeDir ?? homedir(); const homeDir = resolveKimiHome(input.homeDir, env, osHomeDir); @@ -103,11 +107,11 @@ export function resolveBootstrapOptions(input: BootstrapInput = {}): IBootstrapO arch: input.arch ?? process.arch, cwd: input.cwd ?? process.cwd(), env, - clientVersion: input.clientVersion ?? 'unknown', + clientIdentity: input.clientIdentity, }; } -export function bootstrapSeed(input: BootstrapInput = {}): ScopeSeed { +export function bootstrapSeed(input: BootstrapInput): ScopeSeed { return [[IBootstrapOptions as ServiceIdentifier, resolveBootstrapOptions(input)]]; } @@ -115,7 +119,7 @@ export interface BootstrapResult { readonly app: Scope; } -export function bootstrap(input: BootstrapInput = {}, extraSeeds: ScopeSeed = []): BootstrapResult { +export function bootstrap(input: BootstrapInput, extraSeeds: ScopeSeed = []): BootstrapResult { const options = resolveBootstrapOptions(input); const app = createAppScope({ extra: [...bootstrapSeed(input), ...storageSeed(options), ...skillSeed(), ...extraSeeds], diff --git a/packages/agent-core-v2/src/app/bootstrap/bootstrapService.ts b/packages/agent-core-v2/src/app/bootstrap/bootstrapService.ts index 32fcfacdf..afeb61eca 100644 --- a/packages/agent-core-v2/src/app/bootstrap/bootstrapService.ts +++ b/packages/agent-core-v2/src/app/bootstrap/bootstrapService.ts @@ -15,6 +15,8 @@ import { basename, join, relative } from 'pathe'; +import type { KimiHostIdentity } from '@moonshot-ai/kimi-code-oauth'; + import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { @@ -32,7 +34,7 @@ export class BootstrapService implements IBootstrapService { readonly osHomeDir: string; readonly homeDir: string; readonly configPath: string; - readonly clientVersion: string; + readonly clientIdentity: KimiHostIdentity; readonly sessionsDir: string; readonly blobsDir: string; readonly storeDir: string; @@ -51,7 +53,7 @@ export class BootstrapService implements IBootstrapService { this.env = options.env; this.homeDir = options.homeDir; this.configPath = options.configPath; - this.clientVersion = options.clientVersion; + this.clientIdentity = options.clientIdentity; this.sessionsDir = join(options.homeDir, 'sessions'); this.blobsDir = join(options.homeDir, 'blobs'); this.storeDir = join(options.homeDir, 'store'); diff --git a/packages/agent-core-v2/src/app/hostIdentity/hostIdentity.ts b/packages/agent-core-v2/src/app/hostIdentity/hostIdentity.ts index 8720222a7..ea9c3c370 100644 --- a/packages/agent-core-v2/src/app/hostIdentity/hostIdentity.ts +++ b/packages/agent-core-v2/src/app/hostIdentity/hostIdentity.ts @@ -2,7 +2,7 @@ * `hostIdentity` domain (L3) — runtime identity of the embedding host. * * Holds process-level overrides the host product (CLI, desktop, …) injects at - * the composition root: `productName` fills the `${product_name}` slot in the + * the composition root: `displayName` fills the `${product_name}` slot in the * base system-prompt template, `replyStyleGuide` replaces the * `${reply_style_guide}` block (the CLI default describes Markdown rendering * in a terminal). Composition roots set them through {@link hostIdentitySeed}; @@ -13,8 +13,8 @@ import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation'; import { LifecycleScope, registerScopedService, ScopeActivation, type ScopeSeed } from '#/_base/di/scope'; -export interface HostIdentityOverrides { - readonly productName?: string; +export interface PromptIdentityOverrides { + readonly displayName?: string; readonly replyStyleGuide?: string; } @@ -36,13 +36,13 @@ export class HostIdentity implements IHostIdentity { ) {} } -export function hostIdentitySeed(overrides: HostIdentityOverrides | undefined): ScopeSeed { +export function hostIdentitySeed(overrides: PromptIdentityOverrides | undefined): ScopeSeed { if (overrides === undefined) return []; - if (overrides.productName === undefined && overrides.replyStyleGuide === undefined) return []; + if (overrides.displayName === undefined && overrides.replyStyleGuide === undefined) return []; return [ [ IHostIdentity as ServiceIdentifier, - new HostIdentity(overrides.productName, overrides.replyStyleGuide), + new HostIdentity(overrides.displayName, overrides.replyStyleGuide), ], ]; } diff --git a/packages/agent-core-v2/src/app/plugin/manager.ts b/packages/agent-core-v2/src/app/plugin/manager.ts index 9e476298b..d5772737a 100644 --- a/packages/agent-core-v2/src/app/plugin/manager.ts +++ b/packages/agent-core-v2/src/app/plugin/manager.ts @@ -12,6 +12,7 @@ import path from 'node:path'; import { Error2, PluginErrors } from '#/errors'; import type { HookDef } from '#/agent/externalHooks/types'; import type { McpServerConfig } from '#/agent/mcp/config-schema'; +import type { AgentFileRoot } from '#/app/agentFileCatalog/types'; import { discoverFileSkills } from '#/app/skillCatalog/fileSkillDiscovery'; import type { SkillDiscoveryResult } from '#/app/skillCatalog/skillDiscovery'; import type { SkillRoot } from '#/app/skillCatalog/types'; @@ -313,6 +314,17 @@ export class PluginManager { return roots; } + pluginAgentRoots(): readonly AgentFileRoot[] { + const roots: AgentFileRoot[] = []; + for (const record of this.records.values()) { + if (!record.enabled || record.state !== 'ok' || record.manifest === undefined) continue; + for (const dir of record.manifest.agents ?? []) { + roots.push({ path: dir, source: 'plugin' }); + } + } + return roots; + } + enabledSessionStarts(): readonly EnabledPluginSessionStart[] { const out: EnabledPluginSessionStart[] = []; for (const record of this.records.values()) { diff --git a/packages/agent-core-v2/src/app/plugin/manifest.ts b/packages/agent-core-v2/src/app/plugin/manifest.ts index c96afb734..ac3b7ddc0 100644 --- a/packages/agent-core-v2/src/app/plugin/manifest.ts +++ b/packages/agent-core-v2/src/app/plugin/manifest.ts @@ -97,7 +97,7 @@ export async function parseManifest(pluginRoot: string): Promise { @@ -158,7 +168,7 @@ async function resolveSkillsField( } else if (Array.isArray(raw) && raw.every((entry) => typeof entry === 'string')) { entries.push(...raw); } else { - diagnostics.push({ severity: 'error', message: '"skills" must be a string or string[]' }); + diagnostics.push({ severity: 'error', message: `"${field}" must be a string or string[]` }); return []; } @@ -167,7 +177,7 @@ async function resolveSkillsField( if (!entry.startsWith('./')) { diagnostics.push({ severity: 'error', - message: `"skills" path must start with "./" (got "${entry}")`, + message: `"${field}" path must start with "./" (got "${entry}")`, }); continue; } @@ -182,14 +192,14 @@ async function resolveSkillsField( if (!isWithin(real, rootReal)) { diagnostics.push({ severity: 'error', - message: `"skills" path resolves outside the plugin (${entry})`, + message: `"${field}" path resolves outside the plugin (${entry})`, }); continue; } if (!(await isDir(real))) { diagnostics.push({ severity: 'warn', - message: `"skills" path is not a directory (${entry})`, + message: `"${field}" path is not a directory (${entry})`, }); continue; } diff --git a/packages/agent-core-v2/src/app/plugin/plugin.ts b/packages/agent-core-v2/src/app/plugin/plugin.ts index f9be2e090..f9d1162bb 100644 --- a/packages/agent-core-v2/src/app/plugin/plugin.ts +++ b/packages/agent-core-v2/src/app/plugin/plugin.ts @@ -11,6 +11,7 @@ import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiatio import type { Event } from '#/_base/event'; import type { HookDef } from '#/agent/externalHooks/types'; import type { McpServerConfig } from '#/agent/mcp/config-schema'; +import type { AgentFileRoot } from '#/app/agentFileCatalog/types'; import type { SkillRoot } from '#/app/skillCatalog/types'; import type { @@ -59,6 +60,7 @@ export interface IPluginService { listPluginCommands(): Promise; checkUpdates(): Promise; pluginSkillRoots(): Promise; + pluginAgentRoots(): Promise; enabledSessionStarts(): Promise; enabledSystemPrompts(): Promise; enabledMcpServers(): Promise>; diff --git a/packages/agent-core-v2/src/app/plugin/pluginService.ts b/packages/agent-core-v2/src/app/plugin/pluginService.ts index 20e8947aa..83fe5d1bc 100644 --- a/packages/agent-core-v2/src/app/plugin/pluginService.ts +++ b/packages/agent-core-v2/src/app/plugin/pluginService.ts @@ -21,6 +21,7 @@ import { IProviderService } from '#/kosong/provider/provider'; import { ISkillDiscovery } from '#/app/skillCatalog/skillDiscovery'; import type { HookDef } from '#/agent/externalHooks/types'; import type { McpServerConfig } from '#/agent/mcp/config-schema'; +import type { AgentFileRoot } from '#/app/agentFileCatalog/types'; import type { SkillRoot } from '#/app/skillCatalog/types'; import { PluginManager } from './manager'; @@ -158,6 +159,10 @@ export class PluginService extends Disposable implements IPluginService { return this.runConsumptionRead([], async () => this.manager.pluginSkillRoots()); } + pluginAgentRoots(): Promise { + return this.runConsumptionRead([], async () => this.manager.pluginAgentRoots()); + } + enabledSessionStarts(): Promise { return this.runConsumptionRead([], async () => this.manager.enabledSessionStarts()); } diff --git a/packages/agent-core-v2/src/app/plugin/types.ts b/packages/agent-core-v2/src/app/plugin/types.ts index 43de7854e..414f8d3a5 100644 --- a/packages/agent-core-v2/src/app/plugin/types.ts +++ b/packages/agent-core-v2/src/app/plugin/types.ts @@ -34,6 +34,7 @@ export interface PluginManifest { readonly homepage?: string; readonly license?: string; readonly skills?: readonly string[]; + readonly agents?: readonly string[]; readonly sessionStart?: PluginSessionStart; readonly mcpServers?: Readonly>; readonly hooks?: readonly HookDefConfig[]; diff --git a/packages/agent-core-v2/src/app/sessionExport/manifest.ts b/packages/agent-core-v2/src/app/sessionExport/manifest.ts index 49b418987..27f5c80cd 100644 --- a/packages/agent-core-v2/src/app/sessionExport/manifest.ts +++ b/packages/agent-core-v2/src/app/sessionExport/manifest.ts @@ -29,6 +29,7 @@ export function buildExportManifest(args: { readonly sessionLogPath?: string | undefined; readonly globalLogPath?: string | undefined; readonly webLogPath?: string; + readonly desktopVersion?: string; readonly installSource?: string | undefined; readonly shellEnv?: ShellEnvironment | undefined; }): ExportSessionManifest { @@ -52,6 +53,7 @@ export function buildExportManifest(args: { sessionLogPath: args.sessionLogPath, globalLogPath: args.globalLogPath, webLogPath: args.webLogPath, + desktopVersion: args.desktopVersion, installSource: args.installSource, shellEnv: args.shellEnv, }; diff --git a/packages/agent-core-v2/src/app/sessionExport/sessionExport.ts b/packages/agent-core-v2/src/app/sessionExport/sessionExport.ts index a634941e3..df06acd2e 100644 --- a/packages/agent-core-v2/src/app/sessionExport/sessionExport.ts +++ b/packages/agent-core-v2/src/app/sessionExport/sessionExport.ts @@ -23,6 +23,7 @@ export interface ExportSessionPayload { readonly includeGlobalLog?: boolean | undefined; readonly includeDesktopLog?: boolean; readonly version: string; + readonly desktopVersion?: string; readonly installSource?: string | undefined; readonly shellEnv?: ShellEnvironment | undefined; } @@ -42,6 +43,7 @@ export interface ExportSessionManifest { readonly globalLogPath?: string | undefined; readonly desktopLogPath?: string; readonly webLogPath?: string; + readonly desktopVersion?: string; readonly installSource?: string | undefined; readonly shellEnv?: ShellEnvironment | undefined; } diff --git a/packages/agent-core-v2/src/app/sessionExport/sessionExportService.ts b/packages/agent-core-v2/src/app/sessionExport/sessionExportService.ts index 17e59b797..7f97631ba 100644 --- a/packages/agent-core-v2/src/app/sessionExport/sessionExportService.ts +++ b/packages/agent-core-v2/src/app/sessionExport/sessionExportService.ts @@ -215,6 +215,7 @@ export async function exportSessionDirectory(input: { sessionScan, sessionLogPath: stableSessionLog === undefined ? undefined : SESSION_LOG_REL, webLogPath: bundledWebLog ? WEB_LOG_REL : undefined, + desktopVersion: input.request.desktopVersion, installSource: input.request.installSource, shellEnv: input.request.shellEnv, }); diff --git a/packages/agent-core-v2/src/app/sessionIndex/sessionIndexService.ts b/packages/agent-core-v2/src/app/sessionIndex/sessionIndexService.ts index 29f86e54b..b58f9ed59 100644 --- a/packages/agent-core-v2/src/app/sessionIndex/sessionIndexService.ts +++ b/packages/agent-core-v2/src/app/sessionIndex/sessionIndexService.ts @@ -93,6 +93,26 @@ function matchesChildOf(summary: SessionSummary, parentId: string | undefined): ); } +/** + * Runtime shape check for read-model cache hits. The query store persists + * caller-provided JSON with no schema enforcement (and entries mirrored before + * a fix may predate required fields — e.g. `archived` written as `undefined` + * and dropped by JSON), so a cached value is only trusted when it carries the + * fields the session-summary contract requires; anything else is treated as a + * cold miss and rebuilt from disk. + */ +function isSessionSummaryShape(value: unknown): value is SessionSummary { + if (value === null || typeof value !== 'object') return false; + const summary = value as Record; + return ( + typeof summary['id'] === 'string' && + typeof summary['workspaceId'] === 'string' && + typeof summary['createdAt'] === 'number' && + typeof summary['updatedAt'] === 'number' && + typeof summary['archived'] === 'boolean' + ); +} + export class FileSessionIndex implements ISessionIndex { declare readonly _serviceBrand: undefined; @@ -173,8 +193,8 @@ export class FileSessionIndex implements ISessionIndex { } private async getFromReadModel(id: string): Promise { - const cached = await this.queryStore.get(SESSION_COLLECTION, id); - if (cached !== undefined) return cached; + const cached: unknown = await this.queryStore.get(SESSION_COLLECTION, id); + if (isSessionSummaryShape(cached)) return cached; for (const workspaceId of await this.listWorkspaceIds()) { if (!(await this.hasSession(workspaceId, id))) continue; return this.getCachedSummary(workspaceId, id); @@ -217,10 +237,11 @@ export class FileSessionIndex implements ISessionIndex { workspaceId: string, sessionId: string, ): Promise { - const cached = await this.queryStore.get(SESSION_COLLECTION, sessionId); - if (cached !== undefined) return cached; + const cached: unknown = await this.queryStore.get(SESSION_COLLECTION, sessionId); + if (isSessionSummaryShape(cached)) return cached; const summary = await this.readSummary(workspaceId, sessionId); if (summary !== undefined) { + // Also overwrites a cache entry that failed the shape check above. await this.queryStore.put(SESSION_COLLECTION, sessionId, summary); } return summary; diff --git a/packages/agent-core-v2/src/app/telemetry/cloudAppender.ts b/packages/agent-core-v2/src/app/telemetry/cloudAppender.ts index fd0cb5655..fa14f1626 100644 --- a/packages/agent-core-v2/src/app/telemetry/cloudAppender.ts +++ b/packages/agent-core-v2/src/app/telemetry/cloudAppender.ts @@ -3,7 +3,7 @@ * batches events, drops non-primitive properties, redacts PII from string * values, enriches events with common context, and posts them to the * telemetry endpoint through `CloudTransport`, which persists failed events - * through the `storage` byte layer. Reads host facts (`clientVersion`, env, + * through the `storage` byte layer. Reads host facts (`clientIdentity`, env, * platform/arch) from `IBootstrapService`; `createCloudAppender` assembles * one from a `ServicesAccessor` so hosts only supply identity facts. * App-scoped; independent of `@moonshot-ai/kimi-telemetry`. @@ -186,8 +186,8 @@ function buildContext(options: CloudAppenderOptions): CloudContext { const { bootstrap } = options; const context: CloudContext = { app_name: options.appName, - client_version: bootstrap.clientVersion, - version: bootstrap.clientVersion, + client_version: bootstrap.clientIdentity.version, + version: bootstrap.clientIdentity.version, core_version: resolveCoreVersion(), runtime: 'node', platform: bootstrap.platform, diff --git a/packages/agent-core-v2/src/index.ts b/packages/agent-core-v2/src/index.ts index 3cb812560..b989e2764 100644 --- a/packages/agent-core-v2/src/index.ts +++ b/packages/agent-core-v2/src/index.ts @@ -212,6 +212,7 @@ export * from '#/session/sessionSkillCatalog/workspaceFileSkillSource'; export * from '#/session/sessionSkillCatalog/pluginSkillSource'; export * from '#/session/sessionAgentProfileCatalog/sessionAgentProfileCatalog'; export * from '#/session/sessionAgentProfileCatalog/sessionAgentProfileCatalogService'; +export * from '#/session/sessionAgentProfileCatalog/pluginAgentProfileSource'; export * from '#/session/sessionAgentProfileCatalog/projectFileAgentSource'; export * from '#/session/sessionAgentProfileCatalog/extraFileAgentSource'; export * from '#/session/sessionAgentProfileCatalog/explicitFileAgentSource'; @@ -261,7 +262,6 @@ export * from '#/agent/usage/usageService'; export * from '#/agent/toolDedupe/toolDedupe'; export * from '#/agent/toolDedupe/toolDedupeService'; import '#/agent/toolSelect/flag'; -import '#/agent/faultInjection/flag'; export * from '#/agent/tools/select-tools/select-tools'; import '#/agent/tools/select-tools/selectToolsTool'; export * from '#/agent/toolSelect/dynamicTools'; @@ -484,8 +484,6 @@ export * from '#/agent/fullCompaction/compactionOps'; export * from '#/agent/fullCompaction/types'; export * from '#/agent/llmRequester/llmRequester'; export * from '#/agent/llmRequester/llmRequesterService'; -export * from '#/agent/faultInjection/faultInjection'; -export * from '#/agent/faultInjection/faultInjectionService'; export * from '#/agent/llmRequester/llmRequestOps'; export * from '#/_base/utils/retry'; import '#/agent/loop/configSection'; diff --git a/packages/agent-core-v2/src/session/sessionAgentProfileCatalog/pluginAgentProfileSource.ts b/packages/agent-core-v2/src/session/sessionAgentProfileCatalog/pluginAgentProfileSource.ts new file mode 100644 index 000000000..42af97266 --- /dev/null +++ b/packages/agent-core-v2/src/session/sessionAgentProfileCatalog/pluginAgentProfileSource.ts @@ -0,0 +1,74 @@ +/** + * `sessionAgentProfileCatalog` domain (L3) — plugin `IAgentProfileSource` + * producer. + * + * Discovers agent profiles contributed by enabled plugins (roots from + * `plugin.pluginAgentRoots()`), contributing them at priority 5 (above + * builtin, below user / extra / project / explicit, so every other file + * source wins name collisions). `${base_prompt}` is backed by the user + * source's effective default profile. Re-emits `plugin.onDidReload` as + * `onDidChange` so the catalog re-pulls plugin agents when plugins reload; + * install / enable / remove mutations deliberately do not refresh the + * session catalog — those take effect on the next explicit reload. Bound at + * Session scope. + */ + +import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation'; +import type { Event } from '#/_base/event'; +import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; +import { ILogService } from '#/_base/log/log'; +import { discoverAgentFiles } from '#/app/agentFileCatalog/agentFileDiscovery'; +import { + AGENT_PROFILE_SOURCE_PRIORITY, + profilesFromDiscovery, + type AgentProfileContribution, + type IAgentProfileSource, +} from '#/app/agentFileCatalog/agentProfileSource'; +import { IUserFileAgentSource } from '#/app/agentFileCatalog/userFileAgentSource'; +import { IPluginService } from '#/app/plugin/plugin'; +import { IHostFileSystem } from '#/os/interface/hostFileSystem'; + +export interface IPluginAgentProfileSource extends IAgentProfileSource { + readonly _serviceBrand: undefined; +} + +export const IPluginAgentProfileSource: ServiceIdentifier = + createDecorator('pluginAgentProfileSource'); + +export class PluginAgentProfileSource implements IPluginAgentProfileSource { + declare readonly _serviceBrand: undefined; + + readonly id = 'plugin'; + readonly priority = AGENT_PROFILE_SOURCE_PRIORITY.plugin; + readonly onDidChange: Event = (listener, thisArg, disposables) => + this.plugins.onDidReload( + () => listener.call(thisArg, undefined as void), + undefined, + disposables, + ); + + constructor( + @IPluginService private readonly plugins: IPluginService, + @IHostFileSystem private readonly fs: IHostFileSystem, + @ILogService private readonly log: ILogService, + @IUserFileAgentSource private readonly user: IUserFileAgentSource, + ) {} + + async load(): Promise { + const roots = await this.plugins.pluginAgentRoots(); + return profilesFromDiscovery( + await discoverAgentFiles(this.fs, roots, (message) => { + this.log.warn(message); + }), + (context) => this.user.getDefaultProfile().systemPrompt(context), + ); + } +} + +registerScopedService( + LifecycleScope.Session, + IPluginAgentProfileSource, + PluginAgentProfileSource, + ScopeActivation.OnScopeCreated, + 'sessionAgentProfileCatalog', +); diff --git a/packages/agent-core-v2/src/session/sessionAgentProfileCatalog/sessionAgentProfileCatalogService.ts b/packages/agent-core-v2/src/session/sessionAgentProfileCatalog/sessionAgentProfileCatalogService.ts index 920a5cd34..a0612d28a 100644 --- a/packages/agent-core-v2/src/session/sessionAgentProfileCatalog/sessionAgentProfileCatalogService.ts +++ b/packages/agent-core-v2/src/session/sessionAgentProfileCatalog/sessionAgentProfileCatalogService.ts @@ -3,8 +3,9 @@ * implementation. * * Merges the builtin (code-contribution) App catalog with the file-backed - * sources (user / extra / project / explicit) by priority, requiring an - * explicit opt-in before a file replaces a same-name builtin, and serializing + * sources (plugin / user / extra / project / explicit) by priority, requiring + * an explicit opt-in before a file replaces a same-name builtin, and + * serializing * refreshes per source the same way `sessionSkillCatalog` does. The merged * view always contains the builtin profiles (seeded at construction); file * profiles appear once `ready` resolves. A rejecting `fatal` source (an @@ -43,6 +44,7 @@ import { ISessionStateService } from '#/session/state/sessionState'; import { IExplicitFileAgentSource } from './explicitFileAgentSource'; import { IExtraFileAgentSource } from './extraFileAgentSource'; +import { IPluginAgentProfileSource } from './pluginAgentProfileSource'; import { IProjectFileAgentSource } from './projectFileAgentSource'; import { ISessionAgentProfileCatalog } from './sessionAgentProfileCatalog'; @@ -69,6 +71,7 @@ export class SessionAgentProfileCatalogService constructor( @ISessionStateService private readonly states: ISessionStateService, @IAgentProfileCatalogService private readonly builtin: IAgentProfileCatalogService, + @IPluginAgentProfileSource plugin: IPluginAgentProfileSource, @IUserFileAgentSource user: IUserFileAgentSource, @IExtraFileAgentSource extra: IExtraFileAgentSource, @IProjectFileAgentSource project: IProjectFileAgentSource, @@ -78,7 +81,7 @@ export class SessionAgentProfileCatalogService super(); this.states.register(agentProfileCatalogContributionsKey); this.states.register(agentProfileCatalogMergedKey); - this.sources = [user, extra, project, explicit].toSorted( + this.sources = [plugin, user, extra, project, explicit].toSorted( (a, b) => a.priority - b.priority, ); for (const s of this.sources) { diff --git a/packages/agent-core-v2/src/session/sessionMetadata/sessionMetadataService.ts b/packages/agent-core-v2/src/session/sessionMetadata/sessionMetadataService.ts index 9b58cd0f2..d163afa01 100644 --- a/packages/agent-core-v2/src/session/sessionMetadata/sessionMetadataService.ts +++ b/packages/agent-core-v2/src/session/sessionMetadata/sessionMetadataService.ts @@ -143,7 +143,11 @@ export class SessionMetadata extends Disposable implements ISessionMetadata { lastPrompt: this.data.lastPrompt, createdAt: this.data.createdAt, updatedAt: this.data.updatedAt, - archived: this.data.archived, + // `data.archived` stays undefined for sessions whose state.json + // predates the field; the read-model contract requires a boolean + // (`readSummary` normalizes the same way), so an undefined here would + // poison the cache entry and fail contract validation on reads. + archived: this.data.archived === true, custom: this.data.custom, }); } catch (error) { diff --git a/packages/agent-core-v2/test/agent/llmRequester/llmRequesterService.test.ts b/packages/agent-core-v2/test/agent/llmRequester/llmRequesterService.test.ts index 4315bd0a2..f784a4d42 100644 --- a/packages/agent-core-v2/test/agent/llmRequester/llmRequesterService.test.ts +++ b/packages/agent-core-v2/test/agent/llmRequester/llmRequesterService.test.ts @@ -24,8 +24,6 @@ import { type MediaStripSnapshot, } from '#/agent/contextProjector/contextProjector'; import { AgentContextProjectorService } from '#/agent/contextProjector/contextProjectorService'; -import { IFaultInjectionService } from '#/agent/faultInjection/faultInjection'; -import { FaultInjectionService } from '#/agent/faultInjection/faultInjectionService'; import { AgentLLMRequesterService } from '#/agent/llmRequester/llmRequesterService'; import { IAgentLLMRequesterService } from '#/agent/llmRequester/llmRequester'; import { IAgentContextSizeService } from '#/agent/contextSize/contextSize'; @@ -38,7 +36,6 @@ import { IAgentVideoResolverService } from '#/agent/media/videoResolver'; import { IAgentUsageService } from '#/agent/usage/usage'; import { IConfigService } from '#/app/config/config'; import { type DomainEvent, IEventBus } from '#/app/event/eventBus'; -import { IFlagService } from '#/app/flag/flag'; import { APIConnectionError, APIEmptyResponseError, @@ -143,7 +140,6 @@ function createService( >) | undefined, options: { - readonly flagEnabled?: boolean; readonly thinkingLevel?: ThinkingEffort; } = {}, ) { @@ -187,7 +183,6 @@ function createService( shapeTools: (entries) => entries, shapeHistory: (messages) => messages, }; - const flagEnabled = options.flagEnabled ?? true; const testSnapshot = Object.freeze({}) as MediaStripSnapshot; const events: DomainEvent[] = []; const eventBus: IEventBus = { @@ -212,7 +207,6 @@ function createService( ...projector, }); } - ix.stub(IFlagService, { enabled: () => flagEnabled }); ix.stub(IAgentContextSizeService, contextSize); ix.stub(IAgentToolRegistryService, tools); ix.stub(IAgentProfileService, profile); @@ -234,13 +228,11 @@ function createService( log: recordingWireLog(records), eventBus, }); - ix.set(IFaultInjectionService, new SyncDescriptor(FaultInjectionService)); ix.set(IAgentStateService, new AgentStateService()); ix.set(IAgentLLMRequesterService, new SyncDescriptor(AgentLLMRequesterService)); return { service: ix.get(IAgentLLMRequesterService), - faultInjection: ix.get(IFaultInjectionService), wire: ix.get(IWireService), records, events, @@ -623,71 +615,6 @@ describe('AgentLLMRequesterService media-degraded resend', () => { }); }); -describe('AgentLLMRequesterService fault injection (experimental)', () => { - it('raises an armed request-too-large fault before the provider and recovers via the degraded resend', async () => { - const calls = { value: 0 }; - let projectCalls = 0; - let degradedCalls = 0; - const { service, faultInjection } = createService(createRequester(calls, null), { - project: (messages: readonly ContextMessage[]) => { - projectCalls += 1; - return messages; - }, - projectStrict: (messages: readonly ContextMessage[]) => messages, - projectMediaDegraded: (messages: readonly ContextMessage[]) => { - degradedCalls += 1; - return messages; - }, - }); - - faultInjection.arm('request-too-large'); - expect(faultInjection.status().armed).toBe('request-too-large'); - - const result = await service.request({ source: { type: 'turn', turnId: 1, step: 1 } }); - - expect(result.message.content).toEqual([{ type: 'text', text: 'ok' }]); - expect(calls.value).toBe(1); - expect(projectCalls).toBe(1); - expect(degradedCalls).toBe(1); - expect(faultInjection.status()).toEqual({ - armed: undefined, - fired: ['request-too-large'], - }); - }); - - it('raises an armed image-format fault and recovers via the stripped resend, one-shot only', async () => { - const calls = { value: 0 }; - let strippedCalls = 0; - const { service, faultInjection } = createService(createRequester(calls, null), { - project: (messages: readonly ContextMessage[]) => messages, - projectStrict: (messages: readonly ContextMessage[]) => messages, - projectMediaStripped: (messages: readonly ContextMessage[]) => { - strippedCalls += 1; - return messages; - }, - }); - - faultInjection.arm('image-format'); - await service.request({ source: { type: 'turn', turnId: 1, step: 1 } }); - expect(strippedCalls).toBe(1); - expect(faultInjection.status().fired).toEqual(['image-format']); - - const result = await service.request({ source: { type: 'turn', turnId: 2, step: 1 } }); - expect(result.message.content).toEqual([{ type: 'text', text: 'ok' }]); - expect(faultInjection.status().fired).toEqual(['image-format']); - }); - - it('refuses to arm when the fault-injection flag is disabled', () => { - const { faultInjection } = createService(createRequester({ value: 0 }, null), { - project: (messages: readonly ContextMessage[]) => messages, - projectStrict: (messages: readonly ContextMessage[]) => messages, - }, { flagEnabled: false }); - - expect(() => faultInjection.arm('request-too-large')).toThrow(/disabled/); - expect(faultInjection.status()).toEqual({ armed: undefined, fired: [] }); - }); -}); - describe('AgentLLMRequesterService trace id', () => { const passthroughProjector = { project: (messages: readonly ContextMessage[]) => messages, diff --git a/packages/agent-core-v2/test/agent/loop/loop.test.ts b/packages/agent-core-v2/test/agent/loop/loop.test.ts index 8bf8f5a87..78fc57198 100644 --- a/packages/agent-core-v2/test/agent/loop/loop.test.ts +++ b/packages/agent-core-v2/test/agent/loop/loop.test.ts @@ -107,8 +107,8 @@ describe('Agent loop', () => { [emit] turn.step.started { "turnId": 0, "step": 1, "stepId": "" } [emit] agent.activity.updated { "lifecycle": "ready", "turn": { "turnId": 0, "origin": { "kind": "user" }, "phase": "running", "step": 1, "ending": false, "pendingApprovals": [], "activeToolCalls": [], "since": "