@frockbot/plugin-skills 0.3.9 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/agent.test.ts +39 -0
- package/src/agent.ts +36 -7
- package/src/catalog.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-skills",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@frockbot/kernel-agent-loop": "0.3.
|
|
29
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
28
|
+
"@frockbot/kernel-agent-loop": "0.3.11",
|
|
29
|
+
"@frockbot/kernel-contracts": "0.3.11",
|
|
30
30
|
"cordis": "4.0.0-rc.8"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
package/src/agent.test.ts
CHANGED
|
@@ -131,6 +131,45 @@ describe("the skill_load tool", () => {
|
|
|
131
131
|
expect(refused.content).not.toContain("Forbidden body.");
|
|
132
132
|
await dispose();
|
|
133
133
|
});
|
|
134
|
+
|
|
135
|
+
test("accepts the ref field the prompt used to ask for", async () => {
|
|
136
|
+
// The prompt said "call skill_load with a ref"; the schema named the field
|
|
137
|
+
// `path`. `{"ref": ...}` failed `validate` and came back as the loop's
|
|
138
|
+
// generic `Invalid input for tool: skill_load`, naming nothing.
|
|
139
|
+
const workspace = await FakeWorkspace.seeded([
|
|
140
|
+
{
|
|
141
|
+
root: OWN_ROOT,
|
|
142
|
+
path: "skills/kept/SKILL.md",
|
|
143
|
+
text: skillMarkdown("kept", "Use this when keeping.", "Recipe body."),
|
|
144
|
+
writer: BOT_WRITER,
|
|
145
|
+
},
|
|
146
|
+
]);
|
|
147
|
+
const { session, dispose } = await openSession();
|
|
148
|
+
const catalog = new SkillCatalog(OWNER, workspace);
|
|
149
|
+
await catalog.refresh(4, session);
|
|
150
|
+
const tool = createSkillLoadTool(catalog);
|
|
151
|
+
|
|
152
|
+
const loaded = await tool.execute({ ref: "skills/kept/SKILL.md" }, CONTEXT);
|
|
153
|
+
expect(loaded.isError).toBe(false);
|
|
154
|
+
expect(loaded.content).toContain("Recipe body.");
|
|
155
|
+
await dispose();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("says what a wrong input should have been, instead of refusing blankly", async () => {
|
|
159
|
+
const { session, dispose } = await openSession();
|
|
160
|
+
const catalog = new SkillCatalog(OWNER, new FakeWorkspace());
|
|
161
|
+
await catalog.refresh(4, session);
|
|
162
|
+
const tool = createSkillLoadTool(catalog);
|
|
163
|
+
|
|
164
|
+
// A shape the model can produce reaches `execute` and is explained there;
|
|
165
|
+
// `validate` no longer swallows it into a nameless refusal.
|
|
166
|
+
expect(tool.validate?.({ ref: "managed/applets" })).toBe(true);
|
|
167
|
+
const refused = await tool.execute({ skill: "managed/applets" }, CONTEXT);
|
|
168
|
+
expect(refused.isError).toBe(true);
|
|
169
|
+
expect(refused.content).toContain('"path"');
|
|
170
|
+
expect(refused.content).toContain('{"path":"managed/add-connector"}');
|
|
171
|
+
await dispose();
|
|
172
|
+
});
|
|
134
173
|
});
|
|
135
174
|
|
|
136
175
|
describe("the skill_write tool", () => {
|
package/src/agent.ts
CHANGED
|
@@ -273,7 +273,7 @@ const SKILL_LOAD_INPUT_SCHEMA = {
|
|
|
273
273
|
path: {
|
|
274
274
|
type: "string",
|
|
275
275
|
description:
|
|
276
|
-
|
|
276
|
+
'The Skill\'s ref exactly as listed in <agent_skills> — bot/daily-standup, managed/add-connector, or plugin/<packageId>/<slug>. The path listed beside it is also accepted. This field is named "path" whichever of the two you send.',
|
|
277
277
|
},
|
|
278
278
|
},
|
|
279
279
|
required: ["path"],
|
|
@@ -424,6 +424,28 @@ function decodeSkillWriteInputV1(input: unknown): SkillWriteInputV1 {
|
|
|
424
424
|
return decoded;
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
+
/**
|
|
428
|
+
* What `skill_load` was actually asked for, from either field name.
|
|
429
|
+
*
|
|
430
|
+
* The prompt said "call `skill_load` with a ref" while the schema named the
|
|
431
|
+
* field `path`, so the model reached for `ref` and got the loop's generic
|
|
432
|
+
* `Invalid input for tool: skill_load` — no field named, no shape offered. The
|
|
433
|
+
* prompt now names `path`, and `ref` is accepted as an alias so the older
|
|
434
|
+
* phrasing (and the model's own instinct) still lands.
|
|
435
|
+
*/
|
|
436
|
+
export function skillLoadNameV1(input: unknown): string | undefined {
|
|
437
|
+
if (!input || typeof input !== "object") return undefined;
|
|
438
|
+
const record = input as { path?: unknown; ref?: unknown };
|
|
439
|
+
const named = typeof record.path === "string" ? record.path : record.ref;
|
|
440
|
+
if (typeof named !== "string") return undefined;
|
|
441
|
+
const trimmed = named.trim();
|
|
442
|
+
return trimmed.length === 0 ? undefined : trimmed;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Why a `skill_load` input could not be used, and what to send instead. */
|
|
446
|
+
export const SKILL_LOAD_INPUT_REFUSAL =
|
|
447
|
+
'skill_load input is invalid: "path" must be a non-empty string naming a Skill from <agent_skills> — its ref (bot/daily-standup, managed/add-connector, plugin/<packageId>/<slug>) or the path listed beside it. Expected {"path":"managed/add-connector"}.';
|
|
448
|
+
|
|
427
449
|
export function createSkillLoadTool(catalog: SkillCatalog): ToolDefinition {
|
|
428
450
|
return {
|
|
429
451
|
name: "skill_load",
|
|
@@ -432,15 +454,22 @@ export function createSkillLoadTool(catalog: SkillCatalog): ToolDefinition {
|
|
|
432
454
|
// video roles. See `@frockbot/plugin-subagents` `SUBAGENT_TOOL_REACH_V1`.
|
|
433
455
|
admission: { subagentRoles: ["executor"] },
|
|
434
456
|
description:
|
|
435
|
-
|
|
457
|
+
'Read one of your Skills in full. Pass the ref or the path listed in <agent_skills> as "path". Only Skills listed there can be loaded.',
|
|
436
458
|
inputSchema: SKILL_LOAD_INPUT_SCHEMA as unknown as Record<string, unknown>,
|
|
437
459
|
idempotent: true,
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
460
|
+
// Deliberately permissive: a wrong shape reaches `execute`, which says
|
|
461
|
+
// what was wrong. A bare `false` here becomes the generic
|
|
462
|
+
// `Invalid input for tool: skill_load`, which cost a step every time the
|
|
463
|
+
// model reached for the field name the prompt used.
|
|
464
|
+
validate: (input: unknown) => !!input && typeof input === "object",
|
|
442
465
|
execute: (input: unknown) => {
|
|
443
|
-
const named =
|
|
466
|
+
const named = skillLoadNameV1(input);
|
|
467
|
+
if (named === undefined) {
|
|
468
|
+
return Promise.resolve({
|
|
469
|
+
content: SKILL_LOAD_INPUT_REFUSAL,
|
|
470
|
+
isError: true,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
444
473
|
const loaded = catalog.current().skills;
|
|
445
474
|
// A ref first, then the path. Both are printed in `<agent_skills>`, and
|
|
446
475
|
// a ref is the only form that names a managed or plugin Skill, since
|
package/src/catalog.ts
CHANGED
|
@@ -604,7 +604,7 @@ export function renderSkillCatalogPromptV1(catalog: SkillCatalogV1): string {
|
|
|
604
604
|
...entries,
|
|
605
605
|
"</agent_skills>",
|
|
606
606
|
"These are your Skills: recipes you wrote, or your User wrote, for you; the managed ones ship with FrockBot; the plugin ones came with a Package your User installed.",
|
|
607
|
-
|
|
607
|
+
'Only names, refs, paths and descriptions are listed above. Call skill_load with the ref in its "path" field to read a Skill\'s full instructions before you follow it.',
|
|
608
608
|
"Mentioning a Skill is not running it.",
|
|
609
609
|
].join("\n");
|
|
610
610
|
}
|