Skip to content
Closed
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/skill-reload-endpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Add a session skills reload endpoint so clients can pick up newly created user skills without restarting the session.
38 changes: 38 additions & 0 deletions packages/kap-server/src/routes/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
MERGE_ALL_AVAILABLE_SKILLS_SECTION,
SKILL_SOURCE_PRIORITY,
configuredRoots,
programForSession,
projectRoots,
sessionMediaOriginalsDir,
userRoots,
Expand Down Expand Up @@ -84,6 +85,8 @@ const sessionIdParamSchema = z.object({
session_id: z.string().min(1),
});

const SKILLS_RELOAD_SOURCES: readonly string[] = ['user', 'explicit', 'extra'];

const skillTailParamsSchema = z.object({
session_id: z.string().min(1),
tail: z.string().min(1),
Expand Down Expand Up @@ -142,6 +145,41 @@ export function registerSkillsRoutes(app: SkillsRouteHost, core: Scope): void {
listSkillsRoute.handler as Parameters<SkillsRouteHost['get']>[2],
);

const reloadSkillsRoute = defineRoute(
{
method: 'POST',
path: '/sessions/{session_id}/skills::reload',
params: sessionIdParamSchema,
success: { data: listSkillsResponseSchema },
errors: {
[ErrorCode.SESSION_NOT_FOUND]: {},
},
description:
'Reload the user-level skill sources (user / explicit / extra dirs) and return the refreshed skill list',
tags: ['skills'],
operationId: 'reloadSkills',
},
async (req, reply) => {
const { session_id } = req.params;
const resolved = await resolveActivatedSession(core, session_id, req.id);
if ('envelope' in resolved) {
reply.send(resolved.envelope);
return;
}
const program = await programForSession(core.accessor, session_id);
await program?.skills.reloadSources(SKILLS_RELOAD_SOURCES);
Comment thread
liukx0205 marked this conversation as resolved.
Outdated
const catalog = resolved.handle.accessor.get(ISessionSkillCatalog);
await catalog.ready;
const skills = catalog.catalog.listSkills().map(toProtocolSkill);
reply.send(okEnvelope({ skills }, req.id));
},
);
app.post(
reloadSkillsRoute.path,
reloadSkillsRoute.options,
reloadSkillsRoute.handler as Parameters<SkillsRouteHost['post']>[2],
);

const listWorkspaceSkillsRoute = defineRoute(
{
method: 'GET',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,10 @@ exports[`API surface snapshot > matches the documented v2 route table and meta e
"POST",
"/api/v1/sessions/{session_id}/runtime",
],
[
"POST",
"/api/v1/sessions/{session_id}/skills:reload",
],
[
"POST",
"/api/v1/sessions/{session_id}/skills/{tail}",
Expand Down
38 changes: 38 additions & 0 deletions packages/kap-server/test/skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,44 @@ describe('server-v2 /api/v1 skills', () => {
});
});

describe('POST /api/v1/sessions/{sid}/skills:reload', () => {
it('returns 40401 for an unknown session', async () => {
const { body } = await postJson<null>('/api/v1/sessions/nope/skills:reload');
expect(body.code).toBe(40401);
expect(body.msg).toMatch(/does not exist/);
});

it('picks up a user skill created after the session started', async () => {
const id = await createSession();
const skillName = 'e2e-reload-skill';
const before = await getJson<{ skills: SkillWire[] }>(`/api/v1/sessions/${id}/skills`);
expect(before.body.code).toBe(0);
expect(before.body.data.skills.some((s) => s.name === skillName)).toBe(false);

const skillDir = join(home as string, 'skills', skillName);
await mkdir(skillDir, { recursive: true });
await writeFile(
join(skillDir, 'SKILL.md'),
`---\nname: ${skillName}\ndescription: reload test skill\n---\n\nSay hello.\n`,
);
try {
const { body } = await postJson<{ skills: SkillWire[] }>(
`/api/v1/sessions/${id}/skills:reload`,
);
expect(body.code).toBe(0);
const skills = listSkillsResponseSchema.parse(body.data).skills;
const seeded = skills.find((s) => s.name === skillName);
expect(seeded).toBeDefined();
expect(seeded?.source).toBe('user');

const after = await getJson<{ skills: SkillWire[] }>(`/api/v1/sessions/${id}/skills`);
expect(after.body.data.skills.some((s) => s.name === skillName)).toBe(true);
} finally {
await rm(skillDir, { recursive: true, force: true });
}
});
});

describe('GET /api/v1/workspaces/{wid}/skills', () => {
it('lists skills for a workspace without creating a session', async () => {
const workspaceDir = await makeWorkspaceDir();
Expand Down
Loading