Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/host-identity-system-prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/agent-core-v2": patch
---

Let embedding hosts customize the agent's product name and reply-style guidance in the system prompt when starting the server.
6 changes: 5 additions & 1 deletion packages/agent-core-v2/docs/state-manifest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ export interface SessionStateSnapshot {
readonly now?: string;
readonly skills?: string;
readonly skillActive?: boolean;
readonly productName?: string;
readonly replyStyleGuide?: string;
[key: string]: unknown;
}) => string;
readonly promptPrefix?: (ctx: /* AgentProfilePromptPrefixContext — packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts */ {
Expand Down Expand Up @@ -264,6 +266,8 @@ export interface SessionStateSnapshot {
readonly now?: string;
readonly skills?: string;
readonly skillActive?: boolean;
readonly productName?: string;
readonly replyStyleGuide?: string;
[key: string]: unknown;
}) => string;
readonly promptPrefix?: (ctx: /* AgentProfilePromptPrefixContext — packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts */ {
Expand Down Expand Up @@ -1006,7 +1010,7 @@ export interface AgentStateSnapshot {
'llmRequester.lastConfigLogSignature': string | undefined;
'llmRequester.mediaDegradedTurns': Set<number>;
'llmRequester.mediaStrippedTurns': Map<number, /* MediaStripSnapshot — packages/agent-core-v2/src/agent/contextProjector/contextProjector.ts */ {
readonly "__@mediaStripSnapshotBrand@2662": undefined;
readonly "__@mediaStripSnapshotBrand@2667": undefined;
}>;
'llmRequester.turnConfigs': Map<number, /* TurnRequestConfig — packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts */ {
readonly resolved: /* ProfileModelContext — packages/agent-core-v2/src/agent/profile/profile.ts */ {
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core-v2/scripts/check-domain-layers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ const DOMAIN_LAYER = new Map([
['modelCatalog', 3],
['agentProfileCatalog', 3],
['agentFileCatalog', 3],
['hostIdentity', 3],
// L4 — agent behaviour
// `activityView` is the Agent-scope read model folding the agent's own event
// bus into the activity projection (`agent.activity.updated`); it owns no
Expand Down
4 changes: 4 additions & 0 deletions packages/agent-core-v2/src/agent/profile/profileService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import { IAgentTelemetryContextService } from '#/app/telemetry/agentTelemetryCon
import { IWireService } from '#/wire/wire';
import type { PayloadOf } from '#/wire/types';
import { IEventBus } from '#/app/event/eventBus';
import { IHostIdentity } from '#/app/hostIdentity/hostIdentity';
import { prepareSystemPromptContext } from './context';
import type {
ApplyProfileOptions,
Expand Down Expand Up @@ -197,6 +198,7 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ
@IAgentToolRegistryService private readonly toolRegistry: IAgentToolRegistryService,
@IAgentProfileCatalogService private readonly builtinProfiles: IAgentProfileCatalogService,
@IAgentStateService private readonly states: IAgentStateService,
@IHostIdentity private readonly hostIdentity: IHostIdentity,
) {
super();
this.states.register(profileActiveToolNamesOverlayKey);
Expand Down Expand Up @@ -848,6 +850,8 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ
now: new Date().toISOString(),
skills,
skillActive: this.isToolActiveForProfile(profile, 'Skill'),
productName: this.hostIdentity.productName,
replyStyleGuide: this.hostIdentity.replyStyleGuide,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export interface AgentProfileContext {
readonly now?: string;
readonly skills?: string;
readonly skillActive?: boolean;
readonly productName?: string;
readonly replyStyleGuide?: string;
readonly [key: string]: unknown;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
* context fields render as empty strings when missing and the composed
* `*_section` / `windows_notes` blocks are empty unless their content exists,
* so templates can place them on their own line without leaving stray
* headings behind. `renderPromptTemplate` renders a user-owned template (an
* headings behind. Host-identity blocks (`product_name`, `reply_style_guide`)
* work the same way: the context may carry overrides seeded by the embedding
* host (e.g. a desktop app), and the table falls back to the CLI defaults
* ({@link DEFAULT_PRODUCT_NAME}, {@link DEFAULT_REPLY_STYLE_GUIDE}) when it
* does not. `renderPromptTemplate` renders a user-owned template (an
* agent-file body or `SYSTEM.md`) against the table; `${base_prompt}` is
* bound to the default profile's prompt when a `basePrompt` is given,
* resolved lazily and only when the template actually references it. Also
Expand Down Expand Up @@ -61,6 +65,11 @@ export function subagentTypeNotAllowedMessage(
const WINDOWS_NOTES =
'IMPORTANT: You are on Windows. The Bash tool runs through Git Bash, so use Unix shell syntax inside Bash commands — `/dev/null` not `NUL`, and forward slashes in paths. For file operations, always prefer the built-in tools (Read, Write, Edit, Glob, Grep) over Bash commands — they work reliably across all platforms.';

export const DEFAULT_PRODUCT_NAME = 'Kimi Code CLI';

export const DEFAULT_REPLY_STYLE_GUIDE =
"Your text replies render as Markdown in the user's terminal. Use light Markdown that reads well there: short paragraphs, `-` bullets for lists, backticks for code, commands, paths, and identifiers, and fenced blocks for multi-line code. Keep structure shallow — avoid deep nesting, large tables, and heavy headings in ordinary replies. Do not use emoji unless the user does first or asks for it. Default to prose; reach for a list only when the content is genuinely a set of items or steps. When you point to a specific code location, cite it as `path/to/file.ts:42` — a precise, consistent reference the user can navigate to.";

const ADDITIONAL_DIRS_SECTION_PROSE =
'The following directories have been added to the workspace. You can read, write, search, and glob files in these directories as part of your workspace scope.';

Expand All @@ -81,6 +90,8 @@ export function systemPromptVars(
const additionalDirsInfo = context.additionalDirsInfo ?? '';
return {
role_additional: '',
product_name: context.productName ?? DEFAULT_PRODUCT_NAME,
reply_style_guide: context.replyStyleGuide ?? DEFAULT_REPLY_STYLE_GUIDE,
os: context.osKind ?? '',
windows_notes: context.osKind === 'Windows' ? `\n\n${WINDOWS_NOTES}\n\n` : '',
shell: shellName.length > 0 ? `${shellName} (\`${shellPath}\`)` : '',
Expand Down
4 changes: 2 additions & 2 deletions packages/agent-core-v2/src/app/agentProfileCatalog/system.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
You are Kimi Code CLI, an interactive general AI agent running on a user's computer.
You are ${product_name}, an interactive general AI agent running on a user's computer.

Your primary goal is to help users with software engineering tasks by taking action — use the tools available to you to make real changes on the user's system. You should also answer questions when asked. Always adhere strictly to the following system instructions and the user's requirements.

Expand All @@ -18,7 +18,7 @@ When handling the user's request, if it involves creating, modifying, or running

When a dedicated tool fits the job, reach for it before raw shell: `Read` a known path, `Glob` to find files by name, and `Grep` to search file contents. These resolve paths through the workspace access policy and cap their output, so they keep large raw dumps out of the conversation.

Your text replies render as Markdown in the user's terminal. Use light Markdown that reads well there: short paragraphs, `-` bullets for lists, backticks for code, commands, paths, and identifiers, and fenced blocks for multi-line code. Keep structure shallow — avoid deep nesting, large tables, and heavy headings in ordinary replies. Do not use emoji unless the user does first or asks for it. Default to prose; reach for a list only when the content is genuinely a set of items or steps. When you point to a specific code location, cite it as `path/to/file.ts:42` — a precise, consistent reference the user can navigate to.
${reply_style_guide}

You have the capability to output any number of tool calls in a single response. If you anticipate making multiple non-interfering tool calls, you are HIGHLY RECOMMENDED to make them in parallel to significantly improve efficiency. This is very important to your performance. This applies especially to read-only investigation — issue independent `Read`, `Grep`, and `Glob` calls in parallel rather than one after another.

Expand Down
56 changes: 56 additions & 0 deletions packages/agent-core-v2/src/app/hostIdentity/hostIdentity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* `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
* 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};
* the registered default carries no overrides, so the template renders its CLI
* defaults. Bound at App scope.
*/

import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import { LifecycleScope, registerScopedService, ScopeActivation, type ScopeSeed } from '#/_base/di/scope';

export interface HostIdentityOverrides {
readonly productName?: string;
readonly replyStyleGuide?: string;
}

export interface IHostIdentity {
readonly _serviceBrand: undefined;
readonly productName?: string;
readonly replyStyleGuide?: string;
}

export const IHostIdentity: ServiceIdentifier<IHostIdentity> =
createDecorator<IHostIdentity>('hostIdentity');

export class HostIdentity implements IHostIdentity {
declare readonly _serviceBrand: undefined;

constructor(
readonly productName?: string,
readonly replyStyleGuide?: string,
) {}
}

export function hostIdentitySeed(overrides: HostIdentityOverrides | undefined): ScopeSeed {
if (overrides === undefined) return [];
if (overrides.productName === undefined && overrides.replyStyleGuide === undefined) return [];
return [
[
IHostIdentity as ServiceIdentifier<unknown>,
new HostIdentity(overrides.productName, overrides.replyStyleGuide),
],
];
}

registerScopedService(
LifecycleScope.App,
IHostIdentity,
HostIdentity,
ScopeActivation.OnScopeCreated,
'hostIdentity',
);
1 change: 1 addition & 0 deletions packages/agent-core-v2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export * from '#/app/agentFileCatalog/configSection';
export * from '#/app/agentFileCatalog/agentProfileSource';
export * from '#/app/agentFileCatalog/agentCatalogRuntimeOptions';
export * from '#/app/agentFileCatalog/userFileAgentSource';
export * from '#/app/hostIdentity/hostIdentity';
export * from '#/app/plugin/types';
export * from '#/app/plugin/commands';
export * from '#/app/plugin/manifest';
Expand Down
2 changes: 2 additions & 0 deletions packages/agent-core-v2/test/agent/profile/profileOps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { IAgentStateService } from '#/agent/state/agentState';
import { AgentStateService } from '#/agent/state/agentStateService';
import { IHostEnvironment } from '#/os/interface/hostEnvironment';
import { IHostFileSystem } from '#/os/interface/hostFileSystem';
import { IHostIdentity } from '#/app/hostIdentity/hostIdentity';
import { AppendLogStore } from '#/persistence/backends/node-fs/appendLogStore';
import { InMemoryStorageService } from '#/persistence/backends/memory/inMemoryStorageService';
import { IAppendLogStore } from '#/persistence/interface/appendLogStore';
Expand Down Expand Up @@ -210,6 +211,7 @@ function buildHost(key: string): {
host.stub(IProtocolAdapterRegistry, createProtocolRegistryStub());
host.stub(IHostEnvironment, stubUnused());
host.stub(IHostFileSystem, stubUnused());
host.stub(IHostIdentity, stubUnused());
host.stub(IBootstrapService, stubUnused());
host.stub(ISessionContext, createSessionContextStub());
host.stub(ISessionWorkspaceContext, stubUnused());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,23 @@ describe('systemPromptVars', () => {
).toContain('IMPORTANT: You are on Windows');
expect(systemPromptVars({ osKind: 'macOS' }, { skillActive: true })['windows_notes']).toBe('');
});

it('defaults host-identity variables to the CLI text', () => {
const vars = systemPromptVars({}, { skillActive: true });

expect(vars['product_name']).toBe('Kimi Code CLI');
expect(vars['reply_style_guide']).toContain("render as Markdown in the user's terminal");
});

it('lets the context override host-identity variables', () => {
const vars = systemPromptVars(
{ productName: 'Kimi Desktop', replyStyleGuide: 'GUI_STYLE' },
{ skillActive: true },
);

expect(vars['product_name']).toBe('Kimi Desktop');
expect(vars['reply_style_guide']).toBe('GUI_STYLE');
});
});

describe('renderPromptTemplate', () => {
Expand Down Expand Up @@ -183,4 +200,19 @@ describe('renderSystemPrompt', () => {

expect(prompt).not.toMatch(/\$\{[A-Za-z_][A-Za-z0-9_]*\}/);
});

it('renders the host identity from the context, defaulting to the CLI text', () => {
const fallback = renderSystemPrompt('', {}, { skillActive: true });
expect(fallback).toContain('You are Kimi Code CLI,');
expect(fallback).toContain("render as Markdown in the user's terminal");

const overridden = renderSystemPrompt(
'',
{ productName: 'Kimi Desktop', replyStyleGuide: 'GUI_STYLE' },
{ skillActive: true },
);
expect(overridden).toContain('You are Kimi Desktop,');
expect(overridden).toContain('GUI_STYLE');
expect(overridden).not.toContain('Kimi Code CLI');
});
});
11 changes: 11 additions & 0 deletions packages/kap-server/src/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import {
bootstrap,
hostIdentitySeed,
hostRequestHeadersSeed,
IConfigService,
IProviderDiscoveryService,
Expand All @@ -18,6 +19,7 @@ import {
resolveKimiHome,
resolveLoggingConfig,
skillCatalogRuntimeOptionsSeed,
type HostIdentityOverrides,
type Scope,
type ScopeSeed,
} from '@moonshot-ai/agent-core-v2';
Expand Down Expand Up @@ -104,6 +106,14 @@ export interface ServerStartOptions {
readonly rpcToken?: string;
/** Extra scope seeds applied at bootstrap (e.g. a host-provided `ISessionModelResolver`). */
readonly seeds?: ScopeSeed;
/**
* Host product identity injected into the base system prompt: `productName`
* fills the `${product_name}` slot, `replyStyleGuide` replaces the
* `${reply_style_guide}` block. Applied to every agent the server hosts — for
* embedding hosts (e.g. a desktop app), not per-session use. Defaults render
* the CLI text.
*/
readonly hostIdentity?: HostIdentityOverrides;
/**
* Explicit skill directories for this process (v1's SDK `skillDirs`): when
* non-empty, default user / project skill discovery is skipped and these
Expand Down Expand Up @@ -210,6 +220,7 @@ export async function startServer(opts: ServerStartOptions = {}): Promise<Runnin
// through `opts.seeds`, which override this entry (last seed wins).
...hostRequestHeadersSeed({ 'User-Agent': `kimi-code-cli/${hostVersion}` }),
...skillCatalogRuntimeOptionsSeed(opts.skillDirs),
...hostIdentitySeed(opts.hostIdentity),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Seed web hosts with non-terminal prompt identity

When the bundled kimi web path starts this server it does not pass hostIdentity (checked apps/kimi-code/src/cli/sub/web/run.ts:269-288), so this seed is empty and systemPromptVars falls back to the CLI terminal wording. For browser-hosted sessions the model still receives terminal-specific reply guidance, defeating the new host override for an existing non-terminal host; pass a web/GUI identity from that start path or choose a non-terminal default when serving web assets.

AGENTS.md reference: AGENTS.md:L18-L18

Useful? React with 👍 / 👎.

...(opts.seeds ?? []),
]);

Expand Down
Loading