@frockbot/plugin-skills 0.3.8 → 0.3.10

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/plugin-skills",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
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.8",
29
- "@frockbot/kernel-contracts": "0.3.8",
28
+ "@frockbot/kernel-agent-loop": "0.3.10",
29
+ "@frockbot/kernel-contracts": "0.3.10",
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
- "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.",
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
- "Read one of your Skills in full. Pass the path listed in <agent_skills>. Only Skills listed there can be loaded.",
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
- validate: (input: unknown) =>
439
- !!input &&
440
- typeof input === "object" &&
441
- typeof (input as { path?: unknown }).path === "string",
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 = String((input as { path: string }).path).trim();
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
- "Only names, refs, paths and descriptions are listed above. Call skill_load with a ref to read a Skill's full instructions before you follow it.",
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
  }