Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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/qualified-plugin-skills.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix plugin-qualified Skill invocation and report ambiguous bare plugin Skill names.
36 changes: 35 additions & 1 deletion packages/agent-core-v2/docs/state-manifest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,40 @@ export interface SessionStateSnapshot {
readonly mermaid?: string;
readonly d2?: string;
} | undefined;
resolveSkill: (name: string) => /* SkillResolution — packages/agent-core-v2/src/app/skillCatalog/types.ts */ {
readonly kind: 'resolved';
readonly skill: /* SkillDefinition — packages/agent-core-v2/src/app/skillCatalog/types.ts */ {
readonly name: string;
readonly description: string;
readonly path: string;
readonly dir: string;
readonly content: string;
readonly metadata: /* SkillMetadata — packages/agent-core-v2/src/app/skillCatalog/types.ts */ {
readonly name?: string;
readonly description?: string;
readonly type?: string;
readonly whenToUse?: string;
readonly disableModelInvocation?: boolean;
readonly isSubSkill?: boolean;
readonly safe?: boolean;
readonly arguments?: string | readonly unknown[];
[key: string]: unknown;
};
readonly source: /* SkillSource — packages/agent-core-v2/src/app/skillCatalog/types.ts */ 'project' | 'user' | 'extra' | 'builtin';
readonly plugin?: /* SkillPluginContext — packages/agent-core-v2/src/app/skillCatalog/types.ts */ {
readonly id: string;
readonly instructions?: string;
};
readonly mermaid?: string;
readonly d2?: string;
};
readonly canonicalName: string;
} | {
readonly kind: 'not-found';
} | {
readonly kind: 'ambiguous';
readonly candidates: readonly string[];
};
renderSkillPrompt: (skill: /* SkillDefinition — packages/agent-core-v2/src/app/skillCatalog/types.ts */ {
readonly name: string;
readonly description: string;
Expand Down Expand Up @@ -1010,7 +1044,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@2667": undefined;
readonly "__@mediaStripSnapshotBrand@2682": 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
23 changes: 17 additions & 6 deletions packages/agent-core-v2/src/agent/skill/skillService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ import { renderUserSlashSkillPrompt } from './prompt';
import { ISessionContext } from '#/session/sessionContext/sessionContext';
import { Disposable } from '#/_base/di/lifecycle';
import { ErrorCodes, Error2 } from '#/errors';
import { isUserActivatableSkillType, type SkillDefinition } from '#/app/skillCatalog/types';
import {
formatAmbiguousSkillMessage,
isUserActivatableSkillType,
type SkillDefinition,
} from '#/app/skillCatalog/types';
import { IAgentPromptService } from '#/agent/prompt/prompt';
import { ITelemetryService } from '#/app/telemetry/telemetry';
import type { Turn } from '#/agent/loop/loop';
Expand All @@ -45,14 +49,21 @@ export class AgentSkillService extends Disposable implements IAgentSkillService

async activate(input: SkillActivationInput): Promise<Turn> {
await this.skillCatalog.ready;
const skill = this.skillCatalog.catalog.getSkill(input.name);
if (skill === undefined) {
const resolution = this.skillCatalog.catalog.resolveSkill(input.name);
if (resolution.kind === 'not-found') {
throw new Error2(ErrorCodes.SKILL_NOT_FOUND, `Skill "${input.name}" was not found`);
}
if (resolution.kind === 'ambiguous') {
throw new Error2(
ErrorCodes.SKILL_NOT_FOUND,
formatAmbiguousSkillMessage(input.name, resolution.candidates),
);
}
const { canonicalName, skill } = resolution;
if (!isUserActivatableSkillType(skill.metadata.type)) {
throw new Error2(
ErrorCodes.SKILL_TYPE_UNSUPPORTED,
`Skill "${skill.name}" cannot be activated by the user`,
`Skill "${canonicalName}" cannot be activated by the user`,
);
}

Expand All @@ -62,7 +73,7 @@ export class AgentSkillService extends Disposable implements IAgentSkillService
{
type: 'text',
text: renderUserSlashSkillPrompt({
skillName: skill.name,
skillName: canonicalName,
skillArgs,
skillContent,
skillSource: skill.source,
Expand All @@ -75,7 +86,7 @@ export class AgentSkillService extends Disposable implements IAgentSkillService
{
kind: 'skill_activation',
activationId: randomUUID(),
skillName: skill.name,
skillName: canonicalName,
trigger: 'user-slash',
skillType: skill.metadata.type,
skillPath: skill.path,
Expand Down
20 changes: 12 additions & 8 deletions packages/agent-core-v2/src/agent/tools/skill/skillTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { IAgentSkillService } from '#/agent/skill/skill';
import { renderModelToolSkillPrompt } from '#/agent/skill/prompt';
import type { ExecutableToolResult, ToolDeliveryMessage, ToolExecution } from '#/tool/toolContract';
import { registerAgentToolService } from '#/agent/toolRegistry/toolContribution';
import { isInlineSkillType } from '#/app/skillCatalog/types';
import { formatAmbiguousSkillMessage, isInlineSkillType } from '#/app/skillCatalog/types';
import { ISessionSkillCatalog } from '#/session/sessionSkillCatalog/skillCatalog';
import { ISessionContext } from '#/session/sessionContext/sessionContext';
import { renderPrompt } from '#/_base/utils/render-prompt';
Expand Down Expand Up @@ -100,18 +100,22 @@ export async function executeModelSkill(
}

await catalog.ready;
const skill = catalog.catalog.getSkill(args.skill);
if (skill === undefined) {
const resolution = catalog.catalog.resolveSkill(args.skill);
if (resolution.kind === 'not-found') {
return errorResult(`Skill "${args.skill}" not found in the current skill listing.`);
}
if (resolution.kind === 'ambiguous') {
return errorResult(formatAmbiguousSkillMessage(args.skill, resolution.candidates));
}
const { canonicalName, skill } = resolution;
if (skill.metadata.disableModelInvocation === true) {
return errorResult(
`Skill "${args.skill}" can only be triggered by the user (model invocation is disabled).`,
`Skill "${canonicalName}" can only be triggered by the user (model invocation is disabled).`,
);
}
if (!isInlineSkillType(skill.metadata.type)) {
return errorResult(
`Skill "${skill.name}" is not an inline skill and cannot be invoked by the model in v1.`,
`Skill "${canonicalName}" is not an inline skill and cannot be invoked by the model in v1.`,
);
}

Expand All @@ -120,7 +124,7 @@ export async function executeModelSkill(
const origin: SkillActivationOrigin = {
kind: 'skill_activation',
activationId: randomUUID(),
skillName: skill.name,
skillName: canonicalName,
skillArgs: skillArgs.length > 0 ? skillArgs : undefined,
trigger,
skillType: skill.metadata.type,
Expand All @@ -134,7 +138,7 @@ export async function executeModelSkill(
{
type: 'text',
text: renderModelToolSkillPrompt({
skillName: skill.name,
skillName: canonicalName,
skillArgs,
skillContent,
skillSource: skill.source,
Expand All @@ -148,7 +152,7 @@ export async function executeModelSkill(
};
skillService.recordModelToolActivation(origin);
return {
output: `Skill "${skill.name}" loaded inline. Follow its instructions.`,
output: `Skill "${canonicalName}" loaded inline. Follow its instructions.`,
delivery: { kind: 'steer', message },
};
}
Expand Down
67 changes: 61 additions & 6 deletions packages/agent-core-v2/src/app/skillCatalog/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ import type {
SkillCatalog,
SkillDefinition,
SkillMetadata,
SkillResolution,
SkillSource,
SkippedSkill,
} from './types';
import { isInlineSkillType, normalizeSkillName } from './types';
import { canonicalSkillName, isInlineSkillType, normalizeSkillName } from './types';

const LISTING_DESC_MAX = 250;

Expand Down Expand Up @@ -68,6 +69,42 @@ export class InMemorySkillCatalog implements SkillCatalog {
return this.byPluginAndName.get(pluginSkillKey(pluginId, name));
}

resolveSkill(name: string): SkillResolution {
const separator = name.indexOf(':');
if (separator > 0 && separator < name.length - 1) {
const pluginSkill = this.getPluginSkill(
name.slice(0, separator),
name.slice(separator + 1),
);
if (pluginSkill !== undefined) {
return {
kind: 'resolved',
skill: pluginSkill,
canonicalName: canonicalSkillName(pluginSkill),
};
}
}

const skill = this.getSkill(name);
if (skill === undefined) return { kind: 'not-found' };
if (skill.plugin === undefined) {
return { kind: 'resolved', skill, canonicalName: skill.name };
}

const candidates = this.pluginSkillsNamed(name);
if (candidates.length > 1) {
return {
kind: 'ambiguous',
candidates: candidates.map(canonicalSkillName),
};
Comment thread
EvanWangZ marked this conversation as resolved.
Resolved
}
return {
kind: 'resolved',
skill,
canonicalName: canonicalSkillName(skill),
};
}

renderSkillPrompt(
skill: SkillDefinition,
rawArgs: string,
Expand Down Expand Up @@ -95,10 +132,12 @@ export class InMemorySkillCatalog implements SkillCatalog {
}

listInvocableSkills(): readonly SkillDefinition[] {
return this.listSkills().filter(
(skill) =>
skill.metadata.disableModelInvocation !== true && isInlineSkillType(skill.metadata.type),
);
return this.listInvocationCandidates()
.filter(
(skill) =>
skill.metadata.disableModelInvocation !== true && isInlineSkillType(skill.metadata.type),
)
.toSorted((a, b) => canonicalSkillName(a).localeCompare(canonicalSkillName(b)));
}

getSkillRoots(): readonly string[] {
Expand Down Expand Up @@ -136,6 +175,20 @@ export class InMemorySkillCatalog implements SkillCatalog {
this.byPluginAndName.set(key, skill);
}
}

private listInvocationCandidates(): readonly SkillDefinition[] {
return [
...[...this.byName.values()].filter((skill) => skill.plugin === undefined),
...this.byPluginAndName.values(),
];
}

private pluginSkillsNamed(name: string): readonly SkillDefinition[] {
const normalizedName = normalizeSkillName(name);
return [...this.byPluginAndName.values()]
.filter((skill) => normalizeSkillName(skill.name) === normalizedName)
.toSorted((a, b) => canonicalSkillName(a).localeCompare(canonicalSkillName(b)));
}
}

interface SkillExpandContext {
Expand Down Expand Up @@ -225,7 +278,9 @@ function formatFullSkill(skill: SkillDefinition): readonly string[] {
}

function formatModelSkill(skill: SkillDefinition): readonly string[] {
const lines = [`- ${skill.name}: ${truncate(skill.description, LISTING_DESC_MAX)}`];
const lines = [
`- ${canonicalSkillName(skill)}: ${truncate(skill.description, LISTING_DESC_MAX)}`,
];
if (typeof skill.metadata.whenToUse === 'string' && skill.metadata.whenToUse.length > 0) {
lines.push(` When to use: ${skill.metadata.whenToUse}`);
}
Expand Down
24 changes: 24 additions & 0 deletions packages/agent-core-v2/src/app/skillCatalog/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ export interface SkillPluginContext {
readonly instructions?: string;
}

export type SkillResolution =
| {
readonly kind: 'resolved';
readonly skill: SkillDefinition;
readonly canonicalName: string;
}
| { readonly kind: 'not-found' }
| {
readonly kind: 'ambiguous';
readonly candidates: readonly string[];
};

export interface SkippedSkill {
readonly path: string;
readonly type: string;
Expand All @@ -55,6 +67,7 @@ export interface SkippedSkill {
export interface SkillCatalog {
getSkill(name: string): SkillDefinition | undefined;
getPluginSkill(pluginId: string, name: string): SkillDefinition | undefined;
resolveSkill(name: string): SkillResolution;
renderSkillPrompt(
skill: SkillDefinition,
rawArgs: string,
Expand All @@ -71,6 +84,17 @@ export function normalizeSkillName(name: string): string {
return name.toLowerCase();
}

export function canonicalSkillName(skill: SkillDefinition): string {
return skill.plugin === undefined ? skill.name : `${skill.plugin.id}:${skill.name}`;
}

export function formatAmbiguousSkillMessage(
name: string,
candidates: readonly string[],
): string {
return `Skill "${name}" is ambiguous. Use one of these qualified names: ${candidates.map((candidate) => `"${candidate}"`).join(', ')}.`;
}

export function isInlineSkillType(type: string | undefined): boolean {
return type === undefined || type === 'prompt' || type === 'inline';
}
Expand Down
Loading