Make the skill runbook limits a general setting
The longest runbook a skill may save and how much of it load_skill hands back are now set on Settings > General (skill_body_max_chars, skill_body_shown_chars), defaulting to AppData.SkillBodyMaxChars and SkillBodyShownChars. Saving, confirming and proposing a skill check the install's limit; a skill saved before it was lowered can still be toggled. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import createExtensionProposal from "@/src/functions/backend/db/extension-proposals/create-extension-proposal";
|
||||
import grabExtensionProposalView from "@/src/functions/backend/db/extension-proposals/grab-extension-proposal-view";
|
||||
import assertSkillBodyFits from "@/src/functions/backend/db/extensions/assert-skill-body-fits";
|
||||
import grabSkill from "@/src/functions/backend/db/extensions/grab-skill";
|
||||
import { SaveSkillRequestSchema } from "@/src/schemas/extension-schema";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
@@ -51,7 +52,7 @@ export default function proposeSkillTool({ user_id, bot_id, session_id, workspac
|
||||
name: z.string().trim().max(80).optional(),
|
||||
slug: z.string().trim().max(80).optional(),
|
||||
description: z.string().trim().max(AppData.SkillDescriptionMaxChars).optional(),
|
||||
body: z.string().max(AppData.SkillBodyMaxChars).optional().describe("The runbook, markdown"),
|
||||
body: z.string().max(AppData.SkillBodyLimitMax).optional().describe("The runbook, markdown"),
|
||||
enabled: z.boolean().optional(),
|
||||
all_bots: z.boolean().optional(),
|
||||
...SkillReviewFields,
|
||||
@@ -73,6 +74,8 @@ export default function proposeSkillTool({ user_id, bot_id, session_id, workspac
|
||||
},
|
||||
});
|
||||
|
||||
if (data.body != current?.body) await assertSkillBodyFits({ body: data.body });
|
||||
|
||||
const id = await createExtensionProposal({
|
||||
user_id,
|
||||
bot_id,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import type { SkillRecord } from "@/src/types";
|
||||
import openBotSkills from "./open-bot-skills";
|
||||
|
||||
const shown_chars = 30000;
|
||||
|
||||
function skill(overrides: Partial<SkillRecord> & { slug: string }): SkillRecord {
|
||||
return {
|
||||
id: 1,
|
||||
@@ -17,18 +18,18 @@ function skill(overrides: Partial<SkillRecord> & { slug: string }): SkillRecord
|
||||
|
||||
describe("openBotSkills", () => {
|
||||
test("no skills means no tool and no note", () => {
|
||||
expect(openBotSkills({ skills: [] })).toEqual({ tools: [], note: "" });
|
||||
expect(openBotSkills({ skills: [], shown_chars })).toEqual({ tools: [], note: "" });
|
||||
});
|
||||
|
||||
test("a disabled skill is left out entirely", () => {
|
||||
const opened = openBotSkills({ skills: [skill({ slug: "seo", enabled: false })] });
|
||||
const opened = openBotSkills({ skills: [skill({ slug: "seo", enabled: false })], shown_chars });
|
||||
|
||||
expect(opened.tools).toHaveLength(0);
|
||||
expect(opened.note).toBe("");
|
||||
});
|
||||
|
||||
test("one tool, named load_skill, whose description lists the slugs", () => {
|
||||
const opened = openBotSkills({ skills: [skill({ slug: "seo" }), skill({ slug: "inbox", id: 2 })] });
|
||||
const opened = openBotSkills({ skills: [skill({ slug: "seo" }), skill({ slug: "inbox", id: 2 })], shown_chars });
|
||||
|
||||
expect(opened.tools).toHaveLength(1);
|
||||
expect(opened.tools[0]!.name).toBe("load_skill");
|
||||
@@ -38,21 +39,21 @@ describe("openBotSkills", () => {
|
||||
});
|
||||
|
||||
test("a built-in that took the name wins", () => {
|
||||
expect(openBotSkills({ skills: [skill({ slug: "seo" })], taken: ["load_skill"] })).toEqual({
|
||||
expect(openBotSkills({ skills: [skill({ slug: "seo" })], taken: ["load_skill"], shown_chars })).toEqual({
|
||||
tools: [],
|
||||
note: "",
|
||||
});
|
||||
});
|
||||
|
||||
test("the tool returns the body of the slug it's given", async () => {
|
||||
const opened = openBotSkills({ skills: [skill({ slug: "seo", body: "1. Pull rankings" })] });
|
||||
const opened = openBotSkills({ skills: [skill({ slug: "seo", body: "1. Pull rankings" })], shown_chars });
|
||||
const output = await opened.tools[0]!.run!({ args: { slug: "seo" }, signal: AbortSignal.timeout(1000) });
|
||||
|
||||
expect(output).toBe("1. Pull rankings");
|
||||
});
|
||||
|
||||
test("an unknown slug fails with the slugs it does have", async () => {
|
||||
const opened = openBotSkills({ skills: [skill({ slug: "seo" })] });
|
||||
const opened = openBotSkills({ skills: [skill({ slug: "seo" })], shown_chars });
|
||||
|
||||
await expect(opened.tools[0]!.run!({ args: { slug: "nope" }, signal: AbortSignal.timeout(1000) })).rejects.toThrow(
|
||||
/no skill called "nope".*seo/,
|
||||
@@ -61,16 +62,24 @@ describe("openBotSkills", () => {
|
||||
|
||||
test("a long body is cut at the limit and says so", async () => {
|
||||
const opened = openBotSkills({
|
||||
skills: [skill({ slug: "seo", body: "x".repeat(AppData.SkillBodyShownChars + 500) })],
|
||||
skills: [skill({ slug: "seo", body: "x".repeat(shown_chars + 500) })],
|
||||
shown_chars,
|
||||
});
|
||||
const output = await opened.tools[0]!.run!({ args: { slug: "seo" }, signal: AbortSignal.timeout(1000) });
|
||||
|
||||
expect(output).toContain("Cut at");
|
||||
expect(output.length).toBeLessThan(AppData.SkillBodyShownChars + 200);
|
||||
expect(output.length).toBeLessThan(shown_chars + 200);
|
||||
});
|
||||
|
||||
test("the cut follows the install's limit", async () => {
|
||||
const opened = openBotSkills({ skills: [skill({ slug: "seo", body: "x".repeat(1500) })], shown_chars: 1000 });
|
||||
const output = await opened.tools[0]!.run!({ args: { slug: "seo" }, signal: AbortSignal.timeout(1000) });
|
||||
|
||||
expect(output).toContain("Cut at 1000 characters");
|
||||
});
|
||||
|
||||
test("an empty body says it is empty rather than returning nothing", async () => {
|
||||
const opened = openBotSkills({ skills: [skill({ slug: "seo", body: "" })] });
|
||||
const opened = openBotSkills({ skills: [skill({ slug: "seo", body: "" })], shown_chars });
|
||||
const output = await opened.tools[0]!.run!({ args: { slug: "seo" }, signal: AbortSignal.timeout(1000) });
|
||||
|
||||
expect(output).toContain("is empty");
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import { SkillsNote } from "@/src/data/harness-prompts";
|
||||
import { LoadSkillToolName } from "@/src/dict/skill-dict";
|
||||
import type { SkillRecord } from "@/src/types";
|
||||
@@ -10,6 +9,11 @@ type Params = {
|
||||
* The skills the run reaches (`grabBotExtensions`).
|
||||
*/
|
||||
skills: SkillRecord[];
|
||||
/**
|
||||
* How much of a runbook `load_skill` hands back at once
|
||||
* (`grabSkillBodyShownChars`).
|
||||
*/
|
||||
shown_chars: number;
|
||||
/**
|
||||
* Names the run's built-in tools already use: they win.
|
||||
*/
|
||||
@@ -30,7 +34,7 @@ export type OpenBotSkills = {
|
||||
* which ones exist. A skill's body costs nothing until the bot asks for it,
|
||||
* so a run that never needs a long runbook never pays for it.
|
||||
*/
|
||||
export default function openBotSkills({ skills, taken = [] }: Params): OpenBotSkills {
|
||||
export default function openBotSkills({ skills, shown_chars, taken = [] }: Params): OpenBotSkills {
|
||||
const enabled = skills.filter((skill) => skill.enabled);
|
||||
|
||||
if (!enabled.length || taken.includes(LoadSkillToolName)) return { tools: [], note: "" };
|
||||
@@ -57,9 +61,9 @@ export default function openBotSkills({ skills, taken = [] }: Params): OpenBotSk
|
||||
|
||||
if (!body) return `The skill ${skill.name} (${skill.slug}) is empty.`;
|
||||
|
||||
if (body.length <= AppData.SkillBodyShownChars) return body;
|
||||
if (body.length <= shown_chars) return body;
|
||||
|
||||
return `${body.slice(0, AppData.SkillBodyShownChars)}\n\n[Cut at ${AppData.SkillBodyShownChars} characters. The skill continues: ask the user to split it if you need the rest.]`;
|
||||
return `${body.slice(0, shown_chars)}\n\n[Cut at ${shown_chars} characters. The skill continues: ask the user to split it if you need the rest.]`;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user