Skip to content

Commit 535252f

Browse files
committed
feat(skills): add workspace-scoped preload policy
1 parent 82af6b7 commit 535252f

26 files changed

Lines changed: 1207 additions & 92 deletions

console/src/api/modules/skill.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,24 @@ describe("skillApi.disableSkill", () => {
206206
});
207207
});
208208

209+
describe("skillApi.updateSkillLoadPolicy", () => {
210+
afterEach(() => vi.clearAllMocks());
211+
212+
it("sends the load policy to the encoded agent Skill path", async () => {
213+
vi.mocked(request).mockResolvedValue({
214+
updated: true,
215+
load_policy: "preload",
216+
});
217+
218+
await skillApi.updateSkillLoadPolicy("my skill", "preload");
219+
220+
expect(request).toHaveBeenCalledWith("/skills/my%20skill/load-policy", {
221+
method: "PUT",
222+
body: JSON.stringify({ load_policy: "preload" }),
223+
});
224+
});
225+
});
226+
209227
// ---------------------------------------------------------------------------
210228
// deleteSkill — DELETE to encoded path
211229
// ---------------------------------------------------------------------------

console/src/api/modules/skill.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
PoolSkillDetail,
1111
SkillAutomationResponse,
1212
SkillAutomationUpdate,
13+
SkillLoadPolicy,
1314
SkillDetail,
1415
SkillSpec,
1516
WorkspaceSkillSummary,
@@ -466,6 +467,15 @@ export const skillApi = {
466467
},
467468
),
468469

470+
updateSkillLoadPolicy: (skillName: string, loadPolicy: SkillLoadPolicy) =>
471+
request<{ updated: boolean; load_policy: SkillLoadPolicy }>(
472+
`/skills/${encodeURIComponent(skillName)}/load-policy`,
473+
{
474+
method: "PUT",
475+
body: JSON.stringify({ load_policy: loadPolicy }),
476+
},
477+
),
478+
469479
updateSkillTags: (skillName: string, tags: string[]) =>
470480
request<{ updated: boolean; tags: string[] }>(
471481
`/skills/${encodeURIComponent(skillName)}/tags`,

console/src/api/types/skill.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ export type SkillSyncStatus =
55
| "not_synced"
66
| "conflict";
77

8+
export type SkillLoadPolicy = "on_demand" | "preload";
9+
810
export interface SkillSpec {
911
name: string;
1012
description?: string;
1113
source: string;
1214
enabled?: boolean;
15+
load_policy?: SkillLoadPolicy;
1316
channels?: string[];
1417
tags?: string[];
1518
last_updated?: string;

console/src/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1075,6 +1075,9 @@
10751075
"zipOnly": "Only .zip files are supported for upload",
10761076
"fileSizeExceeded": "File size exceeds {{limit}}MB limit. Current file: {{size}}MB",
10771077
"channels": "Channels",
1078+
"preload": "Preload for this agent",
1079+
"preloadHint": "Load the full Skill instructions before the agent's first model decision. Other Skills remain on demand; enabled state and channel scope still apply.",
1080+
"preloadedBadge": "Preloaded",
10781081
"allChannels": "All",
10791082
"lastUpdated": "Updated",
10801083
"searchPlaceholder": "Filter by name",

console/src/locales/zh.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,9 @@
915915
"zipOnly": "仅支持上传 .zip 文件",
916916
"fileSizeExceeded": "文件大小超过 {{limit}}MB 限制。当前文件:{{size}}MB",
917917
"channels": "适用频道",
918+
"preload": "为此智能体预加载",
919+
"preloadHint": "在智能体首次做出模型决策前加载完整技能说明;其他技能仍按需加载,并受启用状态和适用频道限制。",
920+
"preloadedBadge": "已预加载",
918921
"allChannels": "所有",
919922
"lastUpdated": "更新时间",
920923
"searchPlaceholder": "按名称筛选",
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type { ReactNode } from "react";
2+
import { render, screen } from "@testing-library/react";
3+
import { describe, expect, it, vi } from "vitest";
4+
import type { SkillSpec } from "../../../../api/types";
5+
6+
vi.mock("@agentscope-ai/design", () => {
7+
const Container = ({ children }: { children?: ReactNode }) => (
8+
<div>{children}</div>
9+
);
10+
return {
11+
Button: Container,
12+
Card: Container,
13+
Checkbox: Container,
14+
Switch: Container,
15+
Tooltip: Container,
16+
};
17+
});
18+
19+
vi.mock("@ant-design/icons", () => {
20+
const Icon = () => <span />;
21+
return {
22+
CalendarFilled: Icon,
23+
FileTextFilled: Icon,
24+
FileZipFilled: Icon,
25+
FilePdfFilled: Icon,
26+
FileWordFilled: Icon,
27+
FileExcelFilled: Icon,
28+
FilePptFilled: Icon,
29+
FileImageFilled: Icon,
30+
CodeFilled: Icon,
31+
EyeOutlined: Icon,
32+
EyeInvisibleOutlined: Icon,
33+
};
34+
});
35+
36+
vi.mock("react-i18next", () => ({
37+
useTranslation: () => ({ t: (key: string) => key }),
38+
}));
39+
40+
vi.mock("@/utils/skill", () => ({
41+
isSkillBuiltin: () => false,
42+
}));
43+
44+
import { SkillCard } from "./SkillCard";
45+
import { SkillListItem } from "./SkillListItem";
46+
47+
const makeSkill = (enabled: boolean, preload: boolean): SkillSpec => ({
48+
name: "resident",
49+
description: "test",
50+
source: "customized",
51+
enabled,
52+
load_policy: preload ? "preload" : "on_demand",
53+
channels: ["all"],
54+
});
55+
56+
describe("preloaded badge", () => {
57+
it.each([
58+
["card", SkillCard],
59+
["list", SkillListItem],
60+
])("shows only for an enabled preloaded %s Skill", (_name, Component) => {
61+
const commonProps = {
62+
onClick: vi.fn(),
63+
onToggleEnabled: vi.fn(),
64+
onDelete: vi.fn(),
65+
};
66+
const renderComponent = (skill: SkillSpec) =>
67+
Component === SkillCard ? (
68+
<SkillCard skill={skill} {...commonProps} />
69+
) : (
70+
<SkillListItem
71+
skill={skill}
72+
batchModeEnabled={false}
73+
isSelected={false}
74+
onSelect={vi.fn()}
75+
{...commonProps}
76+
/>
77+
);
78+
const { rerender } = render(renderComponent(makeSkill(false, true)));
79+
80+
expect(screen.queryByText("skills.preloadedBadge")).toBeNull();
81+
rerender(renderComponent(makeSkill(true, false)));
82+
expect(screen.queryByText("skills.preloadedBadge")).toBeNull();
83+
rerender(renderComponent(makeSkill(true, true)));
84+
expect(screen.getByText("skills.preloadedBadge")).toBeInTheDocument();
85+
});
86+
});

console/src/pages/Agent/Skills/components/SkillCard.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,11 @@ export const SkillCard = React.memo(function SkillCard({
208208
<span className={styles.statusDot} />
209209
{skill.enabled ? t("common.enabled") : t("common.disabled")}
210210
</span>
211+
{skill.enabled && skill.load_policy === "preload" && (
212+
<span className={styles.preloadedBadge}>
213+
{t("skills.preloadedBadge")}
214+
</span>
215+
)}
211216
{batchMode && (
212217
<Checkbox checked={selected} onClick={handleSelectClick} />
213218
)}

console/src/pages/Agent/Skills/components/SkillDrawer.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { useState, useEffect, useCallback, useRef } from "react";
2-
import { Drawer, Form, Input, Button, Select } from "@agentscope-ai/design";
2+
import {
3+
Drawer,
4+
Form,
5+
Input,
6+
Button,
7+
Select,
8+
Switch,
9+
} from "@agentscope-ai/design";
310
import { useAppMessage } from "../../../../hooks/useAppMessage";
411
import { useTranslation } from "react-i18next";
512
import { ThunderboltOutlined, StopOutlined } from "@ant-design/icons";
@@ -57,6 +64,7 @@ export interface SkillDrawerFormValues {
5764
description?: string;
5865
content: string;
5966
enabled?: boolean;
67+
preload?: boolean;
6068
channels?: string[];
6169
tags?: string[];
6270
source?: string;
@@ -128,6 +136,7 @@ export function SkillDrawer({
128136
form.setFieldsValue({
129137
name: editingSkill.name,
130138
content: editingSkill.content,
139+
preload: editingSkill.load_policy === "preload",
131140
channels,
132141
tags: editingSkill.tags || [],
133142
source: editingSkill.source,
@@ -138,6 +147,7 @@ export function SkillDrawer({
138147
setConfigText("{}");
139148
setConfigError("");
140149
form.resetFields();
150+
form.setFieldsValue({ preload: false, channels: ["all"] });
141151
}
142152
}, [editing, editingSkill, form, t]);
143153

@@ -323,6 +333,15 @@ export function SkillDrawer({
323333
<Select mode="multiple" options={CHANNEL_OPTIONS} />
324334
</Form.Item>
325335

336+
<Form.Item
337+
name="preload"
338+
label={t("skills.preload")}
339+
valuePropName="checked"
340+
tooltip={t("skills.preloadHint")}
341+
>
342+
<Switch />
343+
</Form.Item>
344+
326345
<Form.Item
327346
name="tags"
328347
label={t("skillPool.tags")}

console/src/pages/Agent/Skills/components/SkillListItem.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ export function SkillListItem({
6464
{isBuiltin ? t("skills.builtin") : t("skills.custom")}
6565
</span>
6666
<span className={styles.channelBadge}>{channels}</span>
67+
{skill.enabled && skill.load_policy === "preload" && (
68+
<span className={styles.preloadedBadge}>
69+
{t("skills.preloadedBadge")}
70+
</span>
71+
)}
6772
{skill.last_updated && (
6873
<span className={styles.listItemTime}>
6974
{t("skills.lastUpdated")} {dayjs(skill.last_updated).fromNow()}

console/src/pages/Agent/Skills/index.module.less

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,18 @@
217217
flex-shrink: 0;
218218
}
219219

220+
.preloadedBadge {
221+
display: inline-flex;
222+
align-items: center;
223+
padding: 2px 7px;
224+
border-radius: 999px;
225+
background: rgba(255, 127, 22, 0.12);
226+
color: #d85f00;
227+
font-size: 11px;
228+
font-weight: 600;
229+
white-space: nowrap;
230+
}
231+
220232
.cardHeader {
221233
display: flex;
222234
justify-content: space-between;

0 commit comments

Comments
 (0)